Merge pull request #692 from vector-im/feature/fga/room_list_api

Feature/fga/room list api
This commit is contained in:
ganfra 2023-06-28 18:18:34 +02:00 committed by GitHub
commit 7d54652dd7
44 changed files with 911 additions and 727 deletions

View file

@ -22,7 +22,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import coil.Coil import coil.Coil
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
@ -43,6 +45,8 @@ import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.invitelist.api.InviteListEntryPoint
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint
@ -59,10 +63,13 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.MAIN_SPACE
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.sync.SyncState
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
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.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -82,6 +89,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val inviteListEntryPoint: InviteListEntryPoint, private val inviteListEntryPoint: InviteListEntryPoint,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
snackbarDispatcher: SnackbarDispatcher, snackbarDispatcher: SnackbarDispatcher,
) : BackstackNode<LoggedInFlowNode.NavTarget>( ) : BackstackNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -128,6 +136,7 @@ class LoggedInFlowNode @AssistedInject constructor(
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val syncService = inputs.matrixClient.syncService()
private val loggedInFlowProcessor = LoggedInEventProcessor( private val loggedInFlowProcessor = LoggedInEventProcessor(
snackbarDispatcher, snackbarDispatcher,
inputs.matrixClient.roomMembershipObserver(), inputs.matrixClient.roomMembershipObserver(),
@ -142,12 +151,17 @@ class LoggedInFlowNode @AssistedInject constructor(
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) } plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory() val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory) Coil.setImageLoader(imageLoaderFactory)
inputs.matrixClient.startSync()
appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId) appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId)
// TODO We do not support Space yet, so directly navigate to main space // TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(coroutineScope) loggedInFlowProcessor.observeEvents(coroutineScope)
}, },
onResume = {
syncService.startSync()
},
onPause = {
syncService.stopSync()
},
onDestroy = { onDestroy = {
val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory() val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory) Coil.setImageLoader(imageLoaderFactory)
@ -157,6 +171,27 @@ class LoggedInFlowNode @AssistedInject constructor(
loggedInFlowProcessor.stopObserving() loggedInFlowProcessor.stopObserving()
} }
) )
observeSyncStateAndNetworkStatus()
}
private fun observeSyncStateAndNetworkStatus() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
combine(
syncService.syncState,
networkMonitor.connectivity
) { syncState, networkStatus ->
syncState == SyncState.InError && networkStatus == NetworkStatus.Online
}
.distinctUntilChanged()
.collect { restartSync ->
if (restartSync) {
syncService.startSync()
}
}
}
}
} }
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {

View file

@ -18,7 +18,6 @@ package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
@ -89,6 +88,7 @@ class RoomFlowNode @AssistedInject constructor(
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
Timber.v("OnCreate") Timber.v("OnCreate")
inputs.room.open()
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.room) } plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.room) }
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
fetchRoomMembers() fetchRoomMembers()
@ -159,18 +159,8 @@ class RoomFlowNode @AssistedInject constructor(
data class RoomMemberDetails(val userId: UserId) : NavTarget data class RoomMemberDetails(val userId: UserId) : NavTarget
} }
private val timeline = inputs.room.timeline()
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
DisposableEffect(Unit) {
timeline.initialize()
onDispose {
timeline.dispose()
}
}
Children( Children(
navModel = backstack, navModel = backstack,
modifier = modifier, modifier = modifier,

View file

@ -54,8 +54,8 @@ class InviteListPresenter @Inject constructor(
@Composable @Composable
override fun present(): InviteListState { override fun present(): InviteListState {
val invites by client val invites by client
.invitesDataSource .roomSummaryDataSource
.roomSummaries() .inviteRooms()
.collectAsState() .collectAsState()
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) } var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }

View file

@ -46,10 +46,10 @@ class InviteListPresenterTests {
@Test @Test
fun `present - starts empty, adds invites when received`() = runTest { fun `present - starts empty, adds invites when received`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -60,7 +60,7 @@ class InviteListPresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
Truth.assertThat(initialState.inviteList).isEmpty() Truth.assertThat(initialState.inviteList).isEmpty()
invitesDataSource.postRoomSummary(listOf(aRoomSummary())) roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary()))
val withInviteState = awaitItem() val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1) Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
@ -71,10 +71,10 @@ 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 invitesDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -101,10 +101,10 @@ class InviteListPresenterTests {
@Test @Test
fun `present - includes sender details for room invites`() = runTest { fun `present - includes sender details for room invites`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -129,10 +129,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 invitesDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -154,10 +154,10 @@ 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 invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -179,10 +179,10 @@ class InviteListPresenterTests {
@Test @Test
fun `present - hides confirm dialog when cancelling`() = runTest { fun `present - hides confirm dialog when cancelling`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -204,9 +204,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - declines invite after confirming`() = runTest { fun `present - declines invite after confirming`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService()) val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
@ -230,9 +230,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 invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService()) val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
@ -261,9 +261,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - dismisses declining error state`() = runTest { fun `present - dismisses declining error state`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService()) val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
@ -293,9 +293,9 @@ 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 invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService()) val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
@ -316,9 +316,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 invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService()) val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
@ -341,9 +341,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - dismisses accepting error state`() = runTest { fun `present - dismisses accepting error state`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService()) val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
@ -368,11 +368,11 @@ class InviteListPresenterTests {
@Test @Test
fun `present - stores seen invites when received`() = runTest { fun `present - stores seen invites when received`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val store = FakeSeenInvitesStore() val store = FakeSeenInvitesStore()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
store, store,
FakeAnalyticsService(), FakeAnalyticsService(),
@ -383,19 +383,19 @@ class InviteListPresenterTests {
awaitItem() awaitItem()
// When one invite is received, that ID is saved // When one invite is received, that ID is saved
invitesDataSource.postRoomSummary(listOf(aRoomSummary())) roomSummaryDataSource.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
invitesDataSource.postRoomSummary(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2))) roomSummaryDataSource.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
invitesDataSource.postRoomSummary(listOf()) roomSummaryDataSource.postInviteRooms(listOf())
awaitItem() awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEmpty() Truth.assertThat(store.getProvidedRoomIds()).isEmpty()
@ -404,12 +404,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 invitesDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
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(
invitesDataSource = invitesDataSource, roomSummaryDataSource = roomSummaryDataSource,
), ),
store, store,
FakeAnalyticsService(), FakeAnalyticsService(),
@ -419,7 +419,7 @@ class InviteListPresenterTests {
}.test { }.test {
awaitItem() awaitItem()
invitesDataSource.postRoomSummary(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2))) roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
skipItems(1) skipItems(1)
val withInviteState = awaitItem() val withInviteState = awaitItem()
@ -432,7 +432,7 @@ class InviteListPresenterTests {
} }
private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource { private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource {
postRoomSummary( postInviteRooms(
listOf( listOf(
RoomSummary.Filled( RoomSummary.Filled(
RoomSummaryDetails( RoomSummaryDetails(
@ -461,7 +461,7 @@ class InviteListPresenterTests {
} }
private suspend fun FakeRoomSummaryDataSource.withDirectChatInvitation(): FakeRoomSummaryDataSource { private suspend fun FakeRoomSummaryDataSource.withDirectChatInvitation(): FakeRoomSummaryDataSource {
postRoomSummary( postInviteRooms(
listOf( listOf(
RoomSummary.Filled( RoomSummary.Filled(
RoomSummaryDetails( RoomSummaryDetails(

View file

@ -108,7 +108,7 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(null) mutableStateOf(null)
} }
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus) val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()

View file

@ -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.roomSummaries().collectAsState() val summaries by client.roomSummaryDataSource.allRooms().collectAsState()
LaunchedEffect(query, summaries) { LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>() val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()

View file

@ -78,7 +78,7 @@ class ForwardMessagesPresenterTests {
@Test @Test
fun `present - update query`() = runTest { fun `present - update query`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().apply { val roomSummaryDataSource = FakeRoomSummaryDataSource().apply {
postRoomSummary(listOf(RoomSummary.Filled(aRoomSummaryDetail()))) postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
} }
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val presenter = aPresenter(client = client) val presenter = aPresenter(client = client)

View file

@ -16,9 +16,8 @@
package io.element.android.features.networkmonitor.api package io.element.android.features.networkmonitor.api
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow
interface NetworkMonitor { interface NetworkMonitor {
val connectivity: Flow<NetworkStatus> val connectivity: StateFlow<NetworkStatus>
val currentConnectivityStatus: NetworkStatus
} }

View file

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(FlowPreview::class)
package io.element.android.features.networkmonitor.impl package io.element.android.features.networkmonitor.impl
import android.content.Context import android.content.Context
@ -27,63 +29,80 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(scope = AppScope::class) @ContributesBinding(scope = AppScope::class)
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class NetworkMonitorImpl @Inject constructor( class NetworkMonitorImpl @Inject constructor(
@ApplicationContext context: Context @ApplicationContext context: Context,
appCoroutineScope: CoroutineScope,
) : NetworkMonitor { ) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java) private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
private val callback = object : ConnectivityManager.NetworkCallback() { override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
override fun onAvailable(network: Network) {
_connectivity.value = connectivityManager.currentConnectionStatus() /**
Timber.v("Connectivity status (available): ${connectivityManager.currentConnectionStatus()}") * Calling connectivityManager methods synchronously from the callbacks is not safe.
* So instead we just keep the count of active networks, ie. those checking the capability request.
* Debounce the result to avoid quick offline<->online changes.
*/
val callback = object : ConnectivityManager.NetworkCallback() {
private val activeNetworksCount = AtomicInteger(0)
override fun onLost(network: Network) {
if (activeNetworksCount.decrementAndGet() == 0) {
trySendBlocking(NetworkStatus.Offline)
}
}
override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) {
trySendBlocking(NetworkStatus.Online)
}
}
} }
trySendBlocking(connectivityManager.activeNetworkStatus())
override fun onLost(network: Network) {
_connectivity.value = connectivityManager.currentConnectionStatus()
Timber.v("Connectivity status (lost): ${connectivityManager.currentConnectionStatus()}")
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
_connectivity.value = connectivityManager.currentConnectionStatus()
Timber.v("Connectivity status (changed): ${connectivityManager.currentConnectionStatus()}")
}
}
private val _connectivity = MutableStateFlow(NetworkStatus.Online)
override val connectivity: Flow<NetworkStatus> = _connectivity
override val currentConnectivityStatus: NetworkStatus get() = _connectivity.value
init {
listenToConnectionChanges()
}
private fun listenToConnectionChanges() {
val request = NetworkRequest.Builder() val request = NetworkRequest.Builder()
// .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build() .build()
connectivityManager.registerNetworkCallback(request, callback)
_connectivity.tryEmit(connectivityManager.currentConnectionStatus()) connectivityManager.registerNetworkCallback(request, callback)
Timber.d("Subscribe")
awaitClose {
Timber.d("Unsubscribe")
connectivityManager.unregisterNetworkCallback(callback)
}
}
.distinctUntilChanged()
.debounce(300)
.onEach {
Timber.d("NetworkStatus changed=$it")
}
.stateIn(appCoroutineScope, SharingStarted.WhileSubscribed(), connectivityManager.activeNetworkStatus())
private fun ConnectivityManager.activeNetworkStatus(): NetworkStatus {
return activeNetwork?.let {
getNetworkCapabilities(it)?.getNetworkStatus()
} ?: NetworkStatus.Offline
} }
private fun ConnectivityManager.currentConnectionStatus(): NetworkStatus { private fun NetworkCapabilities.getNetworkStatus(): NetworkStatus {
val hasInternet = activeNetwork?.let(::getNetworkCapabilities) val hasInternet = hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false
return if (hasInternet) { return if (hasInternet) {
NetworkStatus.Online NetworkStatus.Online
} else { } else {

View file

@ -18,13 +18,8 @@ package io.element.android.features.networkmonitor.test
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Online) : NetworkMonitor { class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Online) : NetworkMonitor {
override val currentConnectivityStatus: NetworkStatus override val connectivity = MutableStateFlow(initialStatus)
get() = _connectivityStatus.value
private val _connectivityStatus: MutableStateFlow<NetworkStatus> = MutableStateFlow(initialStatus)
override val connectivity: Flow<NetworkStatus> = _connectivityStatus
} }

View file

@ -43,8 +43,8 @@ class DefaultInviteStateDataSource @Inject constructor(
@Composable @Composable
override fun inviteState(): InvitesState { override fun inviteState(): InvitesState {
val invites by client val invites by client
.invitesDataSource .roomSummaryDataSource
.roomSummaries() .inviteRooms()
.collectAsState() .collectAsState()
val seenInvites by seenInvitesStore val seenInvites by seenInvitesStore

View file

@ -78,10 +78,10 @@ class RoomListPresenter @Inject constructor(
var filter by rememberSaveable { mutableStateOf("") } var filter by rememberSaveable { mutableStateOf("") }
val roomSummaries by client val roomSummaries by client
.roomSummaryDataSource .roomSummaryDataSource
.roomSummaries() .allRooms()
.collectAsState() .collectAsState()
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus) val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
Timber.v("RoomSummaries size = ${roomSummaries.size}") Timber.v("RoomSummaries size = ${roomSummaries.size}")
@ -178,7 +178,7 @@ 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.setSlidingSyncRange(extendedRange) client.roomSummaryDataSource.updateRoomListVisibleRange(extendedRange)
} }
private suspend fun mapRoomSummaries( private suspend fun mapRoomSummaries(

View file

@ -34,8 +34,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 matrixDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@ -48,9 +48,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 matrixDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID))) roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@ -64,9 +64,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 matrixDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2))) roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
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))
@ -81,9 +81,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 matrixDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID))) roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
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))
@ -99,8 +99,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 matrixDataSource = FakeRoomSummaryDataSource() val roomSummaryDataSource = FakeRoomSummaryDataSource()
val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@ -111,7 +111,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
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID))) roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
skipItems(1) skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
@ -121,12 +121,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
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2))) roomSummaryDataSource.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
matrixDataSource.postRoomSummary(emptyList()) roomSummaryDataSource.postInviteRooms(emptyList())
skipItems(1) skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
} }

View file

@ -147,7 +147,7 @@ class RoomListPresenterTests {
// Room list is loaded with 16 placeholders // Room list is loaded with 16 placeholders
Truth.assertThat(withUserState.roomList.size).isEqualTo(16) Truth.assertThat(withUserState.roomList.size).isEqualTo(16)
Truth.assertThat(withUserState.roomList.all { it.isPlaceholder }).isTrue() Truth.assertThat(withUserState.roomList.all { it.isPlaceholder }).isTrue()
roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
skipItems(1) skipItems(1)
val withRoomState = awaitItem() val withRoomState = awaitItem()
Truth.assertThat(withRoomState.roomList.size).isEqualTo(1) Truth.assertThat(withRoomState.roomList.size).isEqualTo(1)
@ -174,7 +174,7 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
skipItems(3) skipItems(3)
val loadedState = awaitItem() val loadedState = awaitItem()
// Test filtering with result // Test filtering with result
@ -212,7 +212,7 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
skipItems(3) skipItems(3)
val loadedState = awaitItem() val loadedState = awaitItem()
// check initial value // check initial value

View file

@ -0,0 +1,41 @@
/*
* 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.core.coroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.job
import kotlinx.coroutines.plus
/**
* Create a child scope of the current scope.
* The child scope will be cancelled if the parent scope is cancelled.
* The child scope will be cancelled if an exception is thrown in the parent scope.
* The parent scope won't be cancelled when an exception is thrown in the child scope.
*
* @param dispatcher the dispatcher to use for this scope.
* @param name the name of the coroutine.
*/
fun CoroutineScope.childScope(
dispatcher: CoroutineDispatcher,
name: String,
): CoroutineScope = run {
val supervisorJob = SupervisorJob(parent = coroutineContext.job)
this + dispatcher + supervisorJob + CoroutineName(name)
}

View file

@ -27,6 +27,7 @@ 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.room.RoomSummaryDataSource
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
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -35,7 +36,6 @@ import java.io.Closeable
interface MatrixClient : Closeable { interface MatrixClient : Closeable {
val sessionId: SessionId val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource val roomSummaryDataSource: RoomSummaryDataSource
val invitesDataSource: RoomSummaryDataSource
val mediaLoader: MatrixMediaLoader val mediaLoader: MatrixMediaLoader
fun getRoom(roomId: RoomId): MatrixRoom? fun getRoom(roomId: RoomId): MatrixRoom?
fun findDM(userId: UserId): MatrixRoom? fun findDM(userId: UserId): MatrixRoom?
@ -45,8 +45,7 @@ interface MatrixClient : Closeable {
suspend fun createDM(userId: UserId): Result<RoomId> suspend fun createDM(userId: UserId): Result<RoomId>
suspend fun getProfile(userId: UserId): Result<MatrixUser> suspend fun getProfile(userId: UserId): Result<MatrixUser>
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
fun startSync() fun syncService(): SyncService
fun stopSync()
fun sessionVerificationService(): SessionVerificationService fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService fun pushersService(): PushersService
fun notificationService(): NotificationService fun notificationService(): NotificationService
@ -60,6 +59,5 @@ interface MatrixClient : Closeable {
suspend fun loadUserDisplayName(): Result<String> suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String?> suspend fun loadUserAvatarURLString(): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun onSlidingSyncUpdate()
fun roomMembershipObserver(): RoomMembershipObserver fun roomMembershipObserver(): RoomMembershipObserver
} }

View file

@ -62,6 +62,8 @@ interface MatrixRoom : Closeable {
fun timeline(): MatrixTimeline fun timeline(): MatrixTimeline
fun open(): Result<Unit>
suspend fun userDisplayName(userId: UserId): Result<String?> suspend fun userDisplayName(userId: UserId): Result<String?>
suspend fun userAvatarUrl(userId: UserId): Result<String?> suspend fun userAvatarUrl(userId: UserId): Result<String?>

View file

@ -19,6 +19,14 @@ package io.element.android.libraries.matrix.api.room
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface RoomSummaryDataSource { interface RoomSummaryDataSource {
fun roomSummaries(): StateFlow<List<RoomSummary>>
fun setSlidingSyncRange(range: IntRange) sealed class LoadingState {
object NotLoaded : LoadingState()
data class Loaded(val numberOfRooms: Int): LoadingState()
}
fun allRoomsLoadingState(): StateFlow<LoadingState>
fun allRooms(): StateFlow<List<RoomSummary>>
fun inviteRooms(): StateFlow<List<RoomSummary>>
fun updateRoomListVisibleRange(range: IntRange)
} }

View file

@ -0,0 +1,36 @@
/*
* 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.sync
import kotlinx.coroutines.flow.StateFlow
interface SyncService {
/**
* Tries to start the sync. If already syncing it has no effect.
*/
fun startSync(): Result<Unit>
/**
* Tries to stop the sync. If service is not syncing it has no effect.
*/
fun stopSync(): Result<Unit>
/**
* Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes.
*/
val syncState: StateFlow<SyncState>
}

View file

@ -0,0 +1,24 @@
/*
* 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.sync
enum class SyncState {
Idle,
Syncing,
InError,
Terminated,
}

View file

@ -28,20 +28,8 @@ interface MatrixTimeline {
) )
fun paginationState(): StateFlow<PaginationState> fun paginationState(): StateFlow<PaginationState>
fun timelineItems(): Flow<List<MatrixTimelineItem>> fun timelineItems(): Flow<List<MatrixTimelineItem>>
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
fun initialize()
fun dispose()
/**
* @param message markdown message
*/
suspend fun sendMessage(message: String): Result<Unit>
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit>
suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit>
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
} }

View file

@ -14,13 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.matrix.impl package io.element.android.libraries.matrix.impl
import io.element.android.libraries.androidutils.file.getSizeOfFiles import io.element.android.libraries.androidutils.file.getSizeOfFiles
import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -31,11 +30,11 @@ import io.element.android.libraries.matrix.api.createroom.RoomVisibility
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.notification.NotificationService 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.ForwardEventException
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.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.timeline.item.event.EventType 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 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
@ -46,7 +45,8 @@ 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.room.RustRoomSummaryDataSource
import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy import io.element.android.libraries.matrix.impl.room.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.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
@ -54,10 +54,7 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -66,18 +63,9 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.RoomMessageEventContent
import org.matrix.rustcomponents.sdk.SlidingSyncList
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
@ -85,7 +73,7 @@ import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
class RustMatrixClient constructor( class RustMatrixClient constructor(
private val client: Client, private val client: Client,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val coroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val baseDirectory: File, private val baseDirectory: File,
private val baseCacheDirectory: File, private val baseCacheDirectory: File,
@ -93,14 +81,16 @@ class RustMatrixClient constructor(
) : MatrixClient { ) : MatrixClient {
override val sessionId: UserId = UserId(client.userId()) override val sessionId: UserId = UserId(client.userId())
private val roomListService = client.roomListServiceWithEncryption()
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService() private val verificationService = RustSessionVerificationService()
private val syncService = RustSyncService(roomListService, sessionCoroutineScope)
private val pushersService = RustPushersService( private val pushersService = RustPushersService(
client = client, client = client,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
private val notificationService = RustNotificationService(client) private val notificationService = RustNotificationService(client)
private var slidingSyncUpdateJob: Job? = null
private val clientDelegate = object : ClientDelegate { private val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) { override fun didReceiveAuthError(isSoftLogout: Boolean) {
@ -109,125 +99,44 @@ class RustMatrixClient constructor(
} }
} }
private val visibleRoomsSlidingSyncFilters = SlidingSyncRequestListFilters(
isDm = null,
spaces = emptyList(),
isEncrypted = null,
isInvite = false,
isTombstoned = false,
roomTypes = emptyList(),
notRoomTypes = listOf("m.space"),
roomNameLike = null,
tags = emptyList(),
notTags = emptyList()
)
private val visibleRoomsSlidingSyncList = MutableSharedFlow<SlidingSyncList>(replay = 1)
private val visibleRoomsSlidingSyncListBuilder = SlidingSyncListBuilder("CurrentlyVisibleRooms")
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
RequiredState(key = EventType.STATE_ROOM_ENCRYPTION, value = ""),
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
)
)
.filters(visibleRoomsSlidingSyncFilters)
.syncModeSelective(SlidingSyncSelectiveModeBuilder().addRange(0u, 20u))
.onceBuilt(object : SlidingSyncListOnceBuilt {
override fun updateList(list: SlidingSyncList): SlidingSyncList {
visibleRoomsSlidingSyncList.tryEmit(list)
return list
}
})
private val invitesSlidingSyncFilters = visibleRoomsSlidingSyncFilters.copy(isInvite = true)
private val invitesSlidingSyncList = MutableSharedFlow<SlidingSyncList>(replay = 1)
private val invitesSlidingSyncListBuilder = SlidingSyncListBuilder("CurrentInvites")
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
RequiredState(key = EventType.STATE_ROOM_ENCRYPTION, value = ""),
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
)
)
.filters(invitesSlidingSyncFilters)
.syncModeSelective(SlidingSyncSelectiveModeBuilder().addRange(0u, 20u))
.onceBuilt(object : SlidingSyncListOnceBuilt {
override fun updateList(list: SlidingSyncList): SlidingSyncList {
invitesSlidingSyncList.tryEmit(list)
return list
}
})
private val slidingSync = client
.slidingSync("ElementX")
// .homeserver("https://slidingsync.lab.matrix.org")
.withCommonExtensions()
.addList(visibleRoomsSlidingSyncListBuilder)
.addList(invitesSlidingSyncListBuilder)
.use {
it.build()
}
private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope)
private val rustRoomSummaryDataSource: RustRoomSummaryDataSource = private val rustRoomSummaryDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource( RustRoomSummaryDataSource(
slidingSyncObserverProxy.updateSummaryFlow, roomListService = roomListService,
slidingSync, sessionCoroutineScope = sessionCoroutineScope,
visibleRoomsSlidingSyncList, coroutineDispatchers = dispatchers,
dispatchers,
) )
override val roomSummaryDataSource: RoomSummaryDataSource override val roomSummaryDataSource: RoomSummaryDataSource
get() = rustRoomSummaryDataSource get() = rustRoomSummaryDataSource
private val rustInvitesDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource(
slidingSyncObserverProxy.updateSummaryFlow,
slidingSync,
invitesSlidingSyncList,
dispatchers,
)
override val invitesDataSource: RoomSummaryDataSource
get() = rustInvitesDataSource
private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client) private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client)
override val mediaLoader: MatrixMediaLoader override val mediaLoader: MatrixMediaLoader
get() = rustMediaLoader get() = rustMediaLoader
private var slidingSyncObserverToken: TaskHandle? = null
private val isSyncing = AtomicBoolean(false)
private val roomMembershipObserver = RoomMembershipObserver() private val roomMembershipObserver = RoomMembershipObserver()
private val roomContentForwarder = RoomContentForwarder(slidingSync) private val roomContentForwarder = RoomContentForwarder(roomListService)
init { init {
client.setDelegate(clientDelegate) client.setDelegate(clientDelegate)
rustRoomSummaryDataSource.init() syncService.syncState
rustInvitesDataSource.init() .onEach { syncState ->
slidingSync.setObserver(slidingSyncObserverProxy) if (syncState == SyncState.Syncing) {
slidingSyncUpdateJob = slidingSyncObserverProxy.updateSummaryFlow onSlidingSyncUpdate()
.onEach { onSlidingSyncUpdate() } }
.launchIn(coroutineScope) }.launchIn(sessionCoroutineScope)
} }
override fun getRoom(roomId: RoomId): MatrixRoom? { override fun getRoom(roomId: RoomId): MatrixRoom? {
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null val roomListItem = roomListService.roomOrNull(roomId.value) ?: return null
val fullRoom = slidingSyncRoom.fullRoom() ?: return null val fullRoom = roomListItem.fullRoom()
return RustMatrixRoom( return RustMatrixRoom(
sessionId = sessionId, sessionId = sessionId,
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, roomListItem = roomListItem,
slidingSyncRoom = slidingSyncRoom,
innerRoom = fullRoom, innerRoom = fullRoom,
coroutineScope = coroutineScope, sessionCoroutineScope = sessionCoroutineScope,
coroutineDispatchers = dispatchers, coroutineDispatchers = dispatchers,
clock = clock, systemClock = clock,
roomContentForwarder = roomContentForwarder, roomContentForwarder = roomContentForwarder,
) )
} }
@ -272,9 +181,11 @@ 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) {
slidingSyncObserverProxy.updateSummaryFlow.filter { roomId.value in it.rooms }.first() roomSummaryDataSource.allRooms()
.filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value)
}.first()
} }
roomId roomId
} }
} }
@ -304,37 +215,19 @@ class RustMatrixClient constructor(
} }
} }
override fun syncService(): SyncService = syncService
override fun sessionVerificationService(): SessionVerificationService = verificationService override fun sessionVerificationService(): SessionVerificationService = verificationService
override fun pushersService(): PushersService = pushersService override fun pushersService(): PushersService = pushersService
override fun notificationService(): NotificationService = notificationService override fun notificationService(): NotificationService = notificationService
override fun startSync() {
if (isSyncing.compareAndSet(false, true)) {
slidingSyncObserverToken = slidingSync.sync()
}
}
override fun stopSync() {
if (isSyncing.compareAndSet(true, false)) {
slidingSyncObserverToken?.use { it.cancel() }
}
}
override fun close() { override fun close() {
slidingSyncUpdateJob?.cancel() sessionCoroutineScope.cancel()
stopSync()
slidingSync.setObserver(null)
rustRoomSummaryDataSource.close()
rustInvitesDataSource.close()
client.setDelegate(null) client.setDelegate(null)
visibleRoomsSlidingSyncListBuilder.destroy()
invitesSlidingSyncListBuilder.destroy()
visibleRoomsSlidingSyncList.resetReplayCache()
invitesSlidingSyncList.resetReplayCache()
slidingSync.destroy()
verificationService.destroy() verificationService.destroy()
roomListService.destroy()
client.destroy() client.destroy()
} }
@ -378,7 +271,7 @@ class RustMatrixClient constructor(
} }
} }
override fun onSlidingSyncUpdate() { private fun onSlidingSyncUpdate() {
if (!verificationService.isReady.value) { if (!verificationService.isReady.value) {
try { try {
verificationService.verificationController = client.getSessionVerificationController() verificationService.verificationController = client.getSessionVerificationController()

View file

@ -52,7 +52,7 @@ import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthentication
class RustMatrixAuthenticationService @Inject constructor( class RustMatrixAuthenticationService @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val baseDirectory: File, private val baseDirectory: File,
private val coroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val clock: SystemClock, private val clock: SystemClock,
@ -180,7 +180,7 @@ class RustMatrixAuthenticationService @Inject constructor(
return RustMatrixClient( return RustMatrixClient(
client = client, client = client,
sessionStore = sessionStore, sessionStore = sessionStore,
coroutineScope = coroutineScope, appCoroutineScope = appCoroutineScope,
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
baseCacheDirectory = context.cacheDir, baseCacheDirectory = context.cacheDir,

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.room.ForwardEventException
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
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.SlidingSync import org.matrix.rustcomponents.sdk.SlidingSync
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener import org.matrix.rustcomponents.sdk.TimelineListener
@ -31,10 +32,10 @@ import kotlin.time.Duration.Companion.milliseconds
/** /**
* Helper to forward event contents from a room to a set of other rooms. * Helper to forward event contents from a room to a set of other rooms.
* @param slidingSync the [SlidingSync] to fetch room instances to forward the event to * @param roomListService the [RoomListService] to fetch room instances to forward the event to
*/ */
class RoomContentForwarder( class RoomContentForwarder(
private val slidingSync: SlidingSync, private val roomListService: RoomListService,
) { ) {
/** /**
@ -51,7 +52,7 @@ class RoomContentForwarder(
timeoutMs: Long = 5000L timeoutMs: Long = 5000L
) { ) {
val content = fromRoom.getTimelineEventContentByEventId(eventId.value) val content = fromRoom.getTimelineEventContentByEventId(eventId.value)
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> slidingSync.getRoom(roomId.value) } val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
val targetRooms = targetSlidingSyncRooms.mapNotNull { slidingSyncRoom -> slidingSyncRoom.use { it.fullRoom() } } val targetRooms = targetSlidingSyncRooms.mapNotNull { slidingSyncRoom -> slidingSyncRoom.use { it.fullRoom() } }
val failedForwardingTo = mutableSetOf<RoomId>() val failedForwardingTo = mutableSetOf<RoomId>()
targetRooms.parallelMap { room -> targetRooms.parallelMap { room ->
@ -79,7 +80,7 @@ class RoomContentForwarder(
} }
} }
private object NoOpTimelineListener: TimelineListener { private object NoOpTimelineListener : TimelineListener {
override fun onUpdate(diff: TimelineDiff) = Unit override fun onUpdate(diff: TimelineDiff) = Unit
} }
} }

View file

@ -0,0 +1,77 @@
/*
* 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.impl.util.mxCallbackFlow
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.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import timber.log.Timber
fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
mxCallbackFlow {
val listener = object : RoomListLoadingStateListener {
override fun onUpdate(state: RoomListLoadingState) {
trySendBlocking(state)
}
}
val result = loadingState(listener)
send(result.state)
result.stateStream
}.buffer(Channel.UNLIMITED)
fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<RoomListEntriesUpdate> =
mxCallbackFlow {
val listener = object : RoomListEntriesListener {
override fun onUpdate(roomEntriesUpdate: RoomListEntriesUpdate) {
trySendBlocking(roomEntriesUpdate)
}
}
val result = entries(listener)
onInitialList(result.entries)
result.entriesStream
}.buffer(Channel.UNLIMITED)
fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
return try {
room(roomId)
} catch (failure: Throwable) {
Timber.e(failure, "Failed finding room with id=$roomId")
return null
}
}
fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
mxCallbackFlow {
val listener = object : RoomListServiceStateListener {
override fun onUpdate(state: RoomListServiceState) {
trySendBlocking(state)
}
}
state(listener)
}.buffer(Channel.UNLIMITED)

View file

@ -20,27 +20,24 @@ 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.room.RoomSummaryDetails
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.SlidingSyncRoom import org.matrix.rustcomponents.sdk.RoomListItem
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) { class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
fun create(slidingSyncRoom: SlidingSyncRoom, room: Room?): RoomSummaryDetails { fun create(roomListItem: RoomListItem, room: Room?): RoomSummaryDetails {
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.use { val latestRoomMessage = roomListItem.latestEvent()?.use {
roomMessageFactory.create(it) roomMessageFactory.create(it)
} }
return RoomSummaryDetails( return RoomSummaryDetails(
roomId = RoomId(slidingSyncRoom.roomId()), roomId = RoomId(roomListItem.id()),
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(), name = roomListItem.name() ?: roomListItem.id(),
canonicalAlias = room?.canonicalAlias(), canonicalAlias = room?.canonicalAlias(),
isDirect = room?.isDirect() ?: false, isDirect = room?.isDirect() ?: false,
avatarURLString = room?.avatarUrl(), avatarURLString = room?.avatarUrl(),
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() }, unreadNotificationCount = roomListItem.unreadNotifications().use { it.notificationCount().toInt() },
lastMessage = latestRoomMessage, lastMessage = latestRoomMessage,
lastMessageTimestamp = latestRoomMessage?.originServerTs, lastMessageTimestamp = latestRoomMessage?.originServerTs,
inviter = room?.inviter()?.let(RoomMemberMapper::map), inviter = room?.inviter()?.let(RoomMemberMapper::map),
) )
} }
} }

View file

@ -0,0 +1,132 @@
/*
* 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 kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListService
import timber.log.Timber
import java.util.UUID
class RoomSummaryListProcessor(
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
private val roomListService: RoomListService,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
private val initLatch = CompletableDeferred<Unit>()
private val mutex = Mutex()
suspend fun postEntries(entries: List<RoomListEntry>) {
updateRoomSummaries {
Timber.v("Update rooms from postEntries (with ${entries.size} items) on ${Thread.currentThread()}")
addAll(entries.map(::buildSummaryForRoomListEntry))
}
initLatch.complete(Unit)
}
suspend fun postUpdate(update: RoomListEntriesUpdate) {
// Makes sure to process first entries before update.
initLatch.await()
updateRoomSummaries {
Timber.v("Update rooms from postUpdate ($update) on ${Thread.currentThread()}")
applyUpdate(update)
}
}
private fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
when (update) {
is RoomListEntriesUpdate.Append -> {
val roomSummaries = update.values.map {
buildSummaryForRoomListEntry(it)
}
addAll(roomSummaries)
}
is RoomListEntriesUpdate.PushBack -> {
val roomSummary = buildSummaryForRoomListEntry(update.value)
add(roomSummary)
}
is RoomListEntriesUpdate.PushFront -> {
val roomSummary = buildSummaryForRoomListEntry(update.value)
add(0, roomSummary)
}
is RoomListEntriesUpdate.Set -> {
val roomSummary = buildSummaryForRoomListEntry(update.value)
this[update.index.toInt()] = roomSummary
}
is RoomListEntriesUpdate.Insert -> {
val roomSummary = buildSummaryForRoomListEntry(update.value)
add(update.index.toInt(), roomSummary)
}
is RoomListEntriesUpdate.Remove -> {
removeAt(update.index.toInt())
}
is RoomListEntriesUpdate.Reset -> {
clear()
addAll(update.values.map { buildSummaryForRoomListEntry(it) })
}
RoomListEntriesUpdate.PopBack -> {
removeLastOrNull()
}
RoomListEntriesUpdate.PopFront -> {
removeFirstOrNull()
}
RoomListEntriesUpdate.Clear -> {
clear()
}
}
}
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
return when (entry) {
RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId)
is RoomListEntry.Invalidated -> {
roomSummariesByIdentifier[entry.roomId] ?: buildEmptyRoomSummary()
}
}
}
private fun buildEmptyRoomSummary(): RoomSummary {
return RoomSummary.Empty(UUID.randomUUID().toString())
}
private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
roomListItem.fullRoom().use { fullRoom ->
RoomSummary.Filled(
details = roomSummaryDetailsFactory.create(roomListItem, fullRoom)
)
}
} ?: buildEmptyRoomSummary()
roomSummariesByIdentifier[builtRoomSummary.identifier()] = builtRoomSummary
return builtRoomSummary
}
private suspend fun updateRoomSummaries(block: MutableList<RoomSummary>.() -> Unit) =
mutex.withLock {
val mutableRoomSummaries = roomSummaries.value.toMutableList()
block(mutableRoomSummaries)
roomSummaries.value = mutableRoomSummaries
}
}

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
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.ProgressCallback import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -32,22 +33,26 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.UpdateSummary
import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber import timber.log.Timber
@ -55,55 +60,80 @@ import java.io.File
class RustMatrixRoom( class RustMatrixRoom(
override val sessionId: SessionId, override val sessionId: SessionId,
private val slidingSyncUpdateFlow: Flow<UpdateSummary>, private val roomListItem: RoomListItem,
private val slidingSyncRoom: SlidingSyncRoom,
private val innerRoom: Room, private val innerRoom: Room,
private val coroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val clock: SystemClock, private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder, private val roomContentForwarder: RoomContentForwarder,
) : MatrixRoom { ) : MatrixRoom {
override val roomId = RoomId(innerRoom.id())
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
override val membersStateFlow: StateFlow<MatrixRoomMembersState> override val membersStateFlow: StateFlow<MatrixRoomMembersState>
get() = _membersStateFlow get() = _membersStateFlow
private var _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown) private var _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
private val isInit = MutableStateFlow(false)
private val syncUpdateFlow = MutableStateFlow(systemClock.epochMillis())
private val timeline by lazy { private val timeline by lazy {
RustMatrixTimeline( RustMatrixTimeline(
matrixRoom = this, matrixRoom = this,
innerRoom = innerRoom, innerRoom = innerRoom,
slidingSyncRoom = slidingSyncRoom, roomCoroutineScope = roomCoroutineScope,
coroutineScope = coroutineScope,
coroutineDispatchers = coroutineDispatchers coroutineDispatchers = coroutineDispatchers
) )
} }
override fun syncUpdateFlow(): Flow<Long> { override fun syncUpdateFlow(): Flow<Long> {
return slidingSyncUpdateFlow return syncUpdateFlow
.filter {
it.rooms.contains(roomId.value)
}
.map {
clock.epochMillis()
}
.onStart { emit(clock.epochMillis()) }
} }
override fun timeline(): MatrixTimeline { override fun timeline(): MatrixTimeline {
return timeline return timeline
} }
override fun close() { override fun open(): Result<Unit> {
innerRoom.destroy() if (isInit.value) return Result.failure(IllegalStateException("Listener already registered"))
slidingSyncRoom.destroy() val settings = RoomSubscription(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
),
timelineLimit = null
)
roomListItem.subscribe(settings)
roomCoroutineScope.launch(coroutineDispatchers.computation) {
innerRoom.timelineDiffFlow { initialList ->
timeline.postItems(initialList)
}.onEach {
syncUpdateFlow.value = systemClock.epochMillis()
timeline.postDiff(it)
}.launchIn(this)
fetchMembers()
}
isInit.value = true
return Result.success(Unit)
} }
override val roomId = RoomId(innerRoom.id()) override fun close() {
if (isInit.value) {
isInit.value = false
roomCoroutineScope.cancel()
roomListItem.unsubscribe()
innerRoom.destroy()
roomListItem.destroy()
}
}
override val name: String? override val name: String?
get() { get() {
return slidingSyncRoom.name() return roomListItem.name()
} }
override val bestName: String override val bestName: String
@ -330,6 +360,12 @@ class RustMatrixRoom(
} }
} }
private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.fetchMembers()
}
}
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching { runCatching {
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)

View file

@ -19,186 +19,104 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.RoomSummary 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.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.impl.sync.roomListDiff
import io.element.android.libraries.matrix.impl.sync.state
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import org.matrix.rustcomponents.sdk.RoomList
import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.SlidingSync import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.SlidingSyncList import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff import org.matrix.rustcomponents.sdk.RoomListRange
import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.SlidingSyncListLoadingState import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.UpdateSummary
import timber.log.Timber import timber.log.Timber
import java.io.Closeable
import java.util.UUID
internal class RustRoomSummaryDataSource( internal class RustRoomSummaryDataSource(
private val slidingSyncUpdateFlow: Flow<UpdateSummary>, private val roomListService: RoomListService,
private val slidingSync: SlidingSync, private val sessionCoroutineScope: CoroutineScope,
private val slidingSyncListFlow: Flow<SlidingSyncList>, coroutineDispatchers: CoroutineDispatchers,
private val coroutineDispatchers: CoroutineDispatchers, roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) : RoomSummaryDataSource {
) : RoomSummaryDataSource, Closeable {
private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) private val allRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val roomSummaries = MutableStateFlow<List<RoomSummary>>(emptyList()) private val allRoomsLoadingState: MutableStateFlow<RoomSummaryDataSource.LoadingState> = MutableStateFlow(RoomSummaryDataSource.LoadingState.NotLoaded)
private val state = MutableStateFlow(SlidingSyncListLoadingState.NOT_LOADED) private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, roomListService, roomSummaryDetailsFactory)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory)
fun init() { init {
coroutineScope.launch { sessionCoroutineScope.launch(coroutineDispatchers.computation) {
val slidingSyncList = slidingSyncListFlow.first() val allRooms = roomListService.allRooms()
val summaries = slidingSyncList.currentRoomList().map(::buildSummaryForRoomListEntry) allRooms
updateRoomSummaries { .observeEntriesWithProcessor(allRoomsListProcessor)
addAll(summaries)
}
slidingSyncList.roomListDiff(this)
.onEach { diffs ->
updateRoomSummaries {
applyDiff(diffs)
}
}
.launchIn(this) .launchIn(this)
slidingSyncList.state(this) allRooms
.onEach { SlidingSyncListLoadingState -> .loadingStateFlow()
Timber.v("New sliding sync state: $SlidingSyncListLoadingState") .map { it.toRoomSummaryDataSourceLoadingState() }
state.value = SlidingSyncListLoadingState .onEach {
allRoomsLoadingState.value = it
}.launchIn(this) }.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)
}
} }
slidingSyncUpdateFlow
.onEach {
didReceiveSyncUpdate(it)
}.launchIn(coroutineScope)
} }
override fun close() { override fun allRooms(): StateFlow<List<RoomSummary>> {
runBlocking { slidingSyncListFlow.firstOrNull() }?.close() return allRooms
coroutineScope.cancel()
} }
override fun roomSummaries(): StateFlow<List<RoomSummary>> { override fun inviteRooms(): StateFlow<List<RoomSummary>> {
return roomSummaries return inviteRooms
} }
override fun setSlidingSyncRange(range: IntRange) { override fun allRoomsLoadingState(): StateFlow<RoomSummaryDataSource.LoadingState> {
return allRoomsLoadingState
}
override fun updateRoomListVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range") Timber.v("setVisibleRange=$range")
coroutineScope.launch { sessionCoroutineScope.launch {
val slidingSyncMode = SlidingSyncSelectiveModeBuilder() try {
.addRange(range.first.toUInt(), range.last.toUInt()) val ranges = listOf(RoomListRange(range.first.toUInt(), range.last.toUInt()))
slidingSyncListFlow.first().setSyncMode(slidingSyncMode) roomListService.applyInput(
} RoomListInput.Viewport(ranges)
} )
} catch (exception: RoomListException) {
private suspend fun didReceiveSyncUpdate(summary: UpdateSummary) { Timber.e(exception, "Failed updating visible range")
Timber.v("UpdateRooms with identifiers: ${summary.rooms}")
if (state.value != SlidingSyncListLoadingState.FULLY_LOADED) {
return
}
updateRoomSummaries {
for (identifier in summary.rooms) {
val index = indexOfFirst { it.identifier() == identifier }
if (index == -1) {
continue
}
val updatedRoomSummary = buildRoomSummaryForIdentifier(identifier)
set(index, updatedRoomSummary)
} }
} }
} }
private fun MutableList<RoomSummary>.applyDiff(diff: SlidingSyncListRoomsListDiff) {
fun MutableList<RoomSummary>.fillUntil(untilIndex: Int) {
repeat((size - 1 until untilIndex).count()) {
add(buildEmptyRoomSummary())
}
}
Timber.v("ApplyDiff: $diff for list with size: $size")
when (diff) {
is SlidingSyncListRoomsListDiff.Append -> {
val roomSummaries = diff.values.map {
buildSummaryForRoomListEntry(it)
}
addAll(roomSummaries)
}
is SlidingSyncListRoomsListDiff.PushBack -> {
val roomSummary = buildSummaryForRoomListEntry(diff.value)
add(roomSummary)
}
is SlidingSyncListRoomsListDiff.PushFront -> {
val roomSummary = buildSummaryForRoomListEntry(diff.value)
add(0, roomSummary)
}
is SlidingSyncListRoomsListDiff.Set -> {
fillUntil(diff.index.toInt())
val roomSummary = buildSummaryForRoomListEntry(diff.value)
set(diff.index.toInt(), roomSummary)
}
is SlidingSyncListRoomsListDiff.Insert -> {
val roomSummary = buildSummaryForRoomListEntry(diff.value)
add(diff.index.toInt(), roomSummary)
}
is SlidingSyncListRoomsListDiff.Remove -> {
removeAt(diff.index.toInt())
}
is SlidingSyncListRoomsListDiff.Reset -> {
clear()
addAll(diff.values.map { buildSummaryForRoomListEntry(it) })
}
SlidingSyncListRoomsListDiff.PopBack -> {
removeFirstOrNull()
}
SlidingSyncListRoomsListDiff.PopFront -> {
removeLastOrNull()
}
SlidingSyncListRoomsListDiff.Clear -> {
clear()
}
}
}
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
return when (entry) {
RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Invalidated -> buildRoomSummaryForIdentifier(entry.roomId)
is RoomListEntry.Filled -> buildRoomSummaryForIdentifier(entry.roomId)
}
}
private fun buildEmptyRoomSummary(): RoomSummary {
return RoomSummary.Empty(UUID.randomUUID().toString())
}
private fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary {
val slidingSyncRoom = slidingSync.getRoom(identifier) ?: return RoomSummary.Empty(identifier)
val fullRoom = slidingSyncRoom.fullRoom()
val roomSummary = RoomSummary.Filled(
details = roomSummaryDetailsFactory.create(slidingSyncRoom, fullRoom)
)
fullRoom?.destroy()
slidingSyncRoom.destroy()
return roomSummary
}
private suspend fun updateRoomSummaries(block: MutableList<RoomSummary>.() -> Unit) =
withContext(coroutineDispatchers.diffUpdateDispatcher) {
val mutableRoomSummaries = roomSummaries.value.toMutableList()
block(mutableRoomSummaries)
roomSummaries.value = mutableRoomSummaries
}
} }
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<RoomListEntriesUpdate> {
return entriesFlow { roomListEntries ->
processor.postEntries(roomListEntries)
}.onEach { update ->
processor.postUpdate(update)
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.sync
import io.element.android.libraries.matrix.api.sync.SyncState
import org.matrix.rustcomponents.sdk.RoomListServiceState
internal fun RoomListServiceState.toSyncState(): SyncState {
return when (this) {
RoomListServiceState.INIT,
RoomListServiceState.SETTING_UP -> SyncState.Idle
RoomListServiceState.RUNNING -> SyncState.Syncing
RoomListServiceState.ERROR -> SyncState.InError
RoomListServiceState.TERMINATED -> SyncState.Terminated
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.sync
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.impl.room.stateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomListServiceState
class RustSyncService(
private val roomListService: RoomListService,
sessionCoroutineScope: CoroutineScope
) : SyncService {
override fun startSync() = runCatching {
if (!roomListService.isSyncing()) {
roomListService.sync()
}
}
override fun stopSync() = runCatching {
if (roomListService.isSyncing()) {
roomListService.stopSync()
}
}
override val syncState: StateFlow<SyncState> =
roomListService
.stateFlow()
.map(RoomListServiceState::toSyncState)
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.WhileSubscribed(), SyncState.Idle)
}

View file

@ -1,62 +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.sync
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.SlidingSyncList
import org.matrix.rustcomponents.sdk.SlidingSyncListLoadingState
import org.matrix.rustcomponents.sdk.SlidingSyncListRoomListObserver
import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsCountObserver
import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff
import org.matrix.rustcomponents.sdk.SlidingSyncListStateObserver
fun SlidingSyncList.roomListDiff(scope: CoroutineScope): Flow<SlidingSyncListRoomsListDiff> =
mxCallbackFlow {
val observer = object : SlidingSyncListRoomListObserver {
override fun didReceiveUpdate(diff: SlidingSyncListRoomsListDiff) {
scope.launch {
send(diff)
}
}
}
observeRoomList(observer)
}
fun SlidingSyncList.state(scope: CoroutineScope): Flow<SlidingSyncListLoadingState> = mxCallbackFlow {
val observer = object : SlidingSyncListStateObserver {
override fun didReceiveUpdate(newState: SlidingSyncListLoadingState) {
scope.launch {
send(newState)
}
}
}
observeState(observer)
}
fun SlidingSyncList.roomsCount(scope: CoroutineScope): Flow<UInt> = mxCallbackFlow {
val observer = object : SlidingSyncListRoomsCountObserver {
override fun didReceiveUpdate(count: UInt) {
scope.launch {
send(count)
}
}
}
observeRoomsCount(observer)
}

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2022 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.sync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.SlidingSyncObserver
import org.matrix.rustcomponents.sdk.UpdateSummary
// Sounds like a reasonable buffer size before it suspends emitting new items.
private const val BUFFER_SIZE = 64
class SlidingSyncObserverProxy(
private val coroutineScope: CoroutineScope,
) : SlidingSyncObserver {
private val updateSummaryMutableFlow =
MutableSharedFlow<UpdateSummary>(extraBufferCapacity = BUFFER_SIZE)
val updateSummaryFlow: SharedFlow<UpdateSummary> = updateSummaryMutableFlow.asSharedFlow()
override fun didReceiveSyncUpdate(summary: UpdateSummary) {
coroutineScope.launch {
updateSummaryMutableFlow.emit(summary)
}
}
}

View file

@ -19,33 +19,38 @@ package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
internal class MatrixTimelineDiffProcessor( internal class MatrixTimelineDiffProcessor(
private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState>, private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState>,
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>, private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>,
private val coroutineScope: CoroutineScope,
private val diffDispatcher: CoroutineDispatcher,
private val timelineItemFactory: MatrixTimelineItemMapper, private val timelineItemFactory: MatrixTimelineItemMapper,
) : TimelineListener { ) {
override fun onUpdate(update: TimelineDiff) { private val initLatch = CompletableDeferred<Unit>()
coroutineScope.launch { private val mutex = Mutex()
updateTimelineItems {
applyDiff(update) suspend fun postItems(items: List<TimelineItem>) {
} updateTimelineItems {
when (val firstItem = timelineItems.value.firstOrNull()) { val mappedItems = items.map { it.asMatrixTimelineItem() }
is MatrixTimelineItem.Virtual -> updateBackPaginationState(firstItem.virtual) addAll(mappedItems)
else -> updateBackPaginationState(null) updateBackPaginationState()
} }
initLatch.complete(Unit)
}
suspend fun postDiff(diff: TimelineDiff) {
// Makes sure to process first items before diff.
initLatch.await()
updateTimelineItems {
applyDiff(diff)
updateBackPaginationState()
} }
} }
@ -69,7 +74,7 @@ internal class MatrixTimelineDiffProcessor(
} }
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) = private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
withContext(diffDispatcher) { mutex.withLock {
val mutableTimelineItems = timelineItems.value.toMutableList() val mutableTimelineItems = timelineItems.value.toMutableList()
block(mutableTimelineItems) block(mutableTimelineItems)
timelineItems.value = mutableTimelineItems timelineItems.value = mutableTimelineItems
@ -120,8 +125,14 @@ internal class MatrixTimelineDiffProcessor(
} }
} }
private fun List<MatrixTimelineItem>.updateBackPaginationState() {
when (val firstItem = firstOrNull()) {
is MatrixTimelineItem.Virtual -> updateBackPaginationState(firstItem.virtual)
else -> updateBackPaginationState(null)
}
}
private fun TimelineItem.asMatrixTimelineItem(): MatrixTimelineItem { private fun TimelineItem.asMatrixTimelineItem(): MatrixTimelineItem {
return timelineItemFactory.map(this) return timelineItemFactory.map(this)
} }
} }

View file

@ -26,7 +26,7 @@ import org.matrix.rustcomponents.sdk.TimelineItem
class MatrixTimelineItemMapper( class MatrixTimelineItemMapper(
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>, private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
private val coroutineScope: CoroutineScope, private val roomCoroutineScope: CoroutineScope,
private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(), private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(),
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(), private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
) { ) {
@ -51,7 +51,7 @@ class MatrixTimelineItemMapper(
return MatrixTimelineItem.Other return MatrixTimelineItem.Other
} }
private fun fetchEventDetails(eventId: EventId) = coroutineScope.launch { private fun fetchEventDetails(eventId: EventId) = roomCoroutineScope.launch {
fetchDetailsForEvent(eventId) fetchDetailsForEvent(eventId)
} }
} }

View file

@ -21,40 +21,30 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.impl.util.TaskHandleBag
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.PaginationOptions import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
class RustMatrixTimeline( class RustMatrixTimeline(
roomCoroutineScope: CoroutineScope,
private val matrixRoom: MatrixRoom, private val matrixRoom: MatrixRoom,
private val innerRoom: Room, private val innerRoom: Room,
private val slidingSyncRoom: SlidingSyncRoom,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixTimeline { ) : MatrixTimeline {
private val isInit = AtomicBoolean(false)
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList()) MutableStateFlow(emptyList())
@ -64,7 +54,7 @@ class RustMatrixTimeline(
private val timelineItemFactory = MatrixTimelineItemMapper( private val timelineItemFactory = MatrixTimelineItemMapper(
fetchDetailsForEvent = this::fetchDetailsForEvent, fetchDetailsForEvent = this::fetchDetailsForEvent,
coroutineScope = coroutineScope, roomCoroutineScope = roomCoroutineScope,
virtualTimelineItemMapper = VirtualTimelineItemMapper(), virtualTimelineItemMapper = VirtualTimelineItemMapper(),
eventTimelineItemMapper = EventTimelineItemMapper( eventTimelineItemMapper = EventTimelineItemMapper(
contentMapper = TimelineEventContentMapper( contentMapper = TimelineEventContentMapper(
@ -73,15 +63,12 @@ class RustMatrixTimeline(
) )
) )
private val innerTimelineListener = MatrixTimelineDiffProcessor( private val timelineDiffProcessor = MatrixTimelineDiffProcessor(
paginationState = paginationState, paginationState = paginationState,
timelineItems = timelineItems, timelineItems = timelineItems,
coroutineScope = coroutineScope,
diffDispatcher = coroutineDispatchers.diffUpdateDispatcher,
timelineItemFactory = timelineItemFactory, timelineItemFactory = timelineItemFactory,
) )
private val listenerTokens = TaskHandleBag()
override fun paginationState(): StateFlow<MatrixTimeline.PaginationState> { override fun paginationState(): StateFlow<MatrixTimeline.PaginationState> {
return paginationState return paginationState
} }
@ -91,43 +78,12 @@ class RustMatrixTimeline(
return timelineItems.sample(50) return timelineItems.sample(50)
} }
override fun initialize() { internal suspend fun postItems(items: List<TimelineItem>) {
Timber.v("Init timeline for room ${matrixRoom.roomId}") timelineDiffProcessor.postItems(items)
coroutineScope.launch {
val result = addListener(innerTimelineListener)
result
.onSuccess { timelineItems ->
val matrixTimelineItems = timelineItems.map(timelineItemFactory::map)
withContext(coroutineDispatchers.diffUpdateDispatcher) {
this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems
}
isInit.set(true)
}
.onFailure {
Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})")
}
}
} }
override fun dispose() { internal suspend fun postDiff(timelineDiff: TimelineDiff) {
Timber.v("Dispose timeline for room ${matrixRoom.roomId}") timelineDiffProcessor.postDiff(timelineDiff)
listenerTokens.dispose()
isInit.set(false)
}
/**
* @param message markdown message
*/
override suspend fun sendMessage(message: String): Result<Unit> {
return matrixRoom.sendMessage(message)
}
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
return matrixRoom.editMessage(originalEventId, message = message)
}
override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
return matrixRoom.replyMessage(inReplyToEventId, message)
} }
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(coroutineDispatchers.io) {
@ -139,9 +95,6 @@ class RustMatrixTimeline(
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching { runCatching {
Timber.v("Start back paginating for room ${matrixRoom.roomId} ") Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
if (!isInit.get()) {
throw IllegalStateException("Timeline is not init yet")
}
val paginationOptions = PaginationOptions.UntilNumItems( val paginationOptions = PaginationOptions.UntilNumItems(
eventLimit = requestSize.toUShort(), eventLimit = requestSize.toUShort(),
items = untilNumberOfItems.toUShort(), items = untilNumberOfItems.toUShort(),
@ -154,31 +107,4 @@ class RustMatrixTimeline(
Timber.v("Success back paginating for room ${matrixRoom.roomId}") Timber.v("Success back paginating for room ${matrixRoom.roomId}")
} }
} }
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
runCatching {
val settings = RoomSubscription(
requiredState = listOf(
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
),
timelineLimit = null
)
slidingSyncRoom.subscribeToRoom(settings)
val result = slidingSyncRoom.addTimelineListener(timelineListener)
launch {
fetchMembers()
}
listenerTokens += result.taskHandle
result.items
}
}
private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.fetchMembers()
}
}
} }

View file

@ -0,0 +1,39 @@
/*
* 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.timeline
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
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.Room
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
internal fun Room.timelineDiffFlow(onInitialList: suspend (List<TimelineItem>) -> Unit): Flow<TimelineDiff> =
mxCallbackFlow {
val listener = object : TimelineListener {
override fun onUpdate(diff: TimelineDiff) {
trySendBlocking(diff)
}
}
val result = addTimelineListener(listener)
onInitialList(result.items)
result.itemsStream
}.buffer(Channel.UNLIMITED)

View file

@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.test.notification.FakeNotificationSer
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.room.FakeRoomSummaryDataSource
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 kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -44,11 +45,11 @@ class FakeMatrixClient(
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 roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
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(),
private val notificationService: FakeNotificationService = FakeNotificationService(), private val notificationService: FakeNotificationService = FakeNotificationService(),
private val syncService: FakeSyncService = FakeSyncService(),
) : MatrixClient { ) : MatrixClient {
private var ignoreUserResult: Result<Unit> = Result.success(Unit) private var ignoreUserResult: Result<Unit> = Result.success(Unit)
@ -98,9 +99,7 @@ class FakeMatrixClient(
return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm")) return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm"))
} }
override fun startSync() = Unit override fun syncService() = syncService
override fun stopSync() = Unit
override suspend fun getCacheSize(): Long { override suspend fun getCacheSize(): Long {
return 0 return 0
@ -138,8 +137,6 @@ class FakeMatrixClient(
override fun notificationService(): NotificationService = notificationService override fun notificationService(): NotificationService = notificationService
override fun onSlidingSyncUpdate() {}
override fun roomMembershipObserver(): RoomMembershipObserver { override fun roomMembershipObserver(): RoomMembershipObserver {
return RoomMembershipObserver() return RoomMembershipObserver()
} }

View file

@ -134,6 +134,10 @@ class FakeMatrixRoom(
return matrixTimeline return matrixTimeline
} }
override fun open(): Result<Unit> {
return Result.success(Unit)
}
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask { override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
userDisplayNameResult userDisplayNameResult
} }

View file

@ -23,20 +23,38 @@ import kotlinx.coroutines.flow.StateFlow
class FakeRoomSummaryDataSource : RoomSummaryDataSource { class FakeRoomSummaryDataSource : RoomSummaryDataSource {
private val roomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList()) private val allRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
private val inviteRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingStateFlow = MutableStateFlow<RoomSummaryDataSource.LoadingState>(RoomSummaryDataSource.LoadingState.NotLoaded)
suspend fun postRoomSummary(roomSummaries: List<RoomSummary>) { suspend fun postAllRooms(roomSummaries: List<RoomSummary>) {
roomSummariesFlow.emit(roomSummaries) allRoomSummariesFlow.emit(roomSummaries)
} }
override fun roomSummaries(): StateFlow<List<RoomSummary>> { suspend fun postInviteRooms(roomSummaries: List<RoomSummary>) {
return roomSummariesFlow inviteRoomSummariesFlow.emit(roomSummaries)
}
suspend fun postLoadingState(loadingState: RoomSummaryDataSource.LoadingState) {
allRoomsLoadingStateFlow.emit(loadingState)
}
override fun allRoomsLoadingState(): StateFlow<RoomSummaryDataSource.LoadingState> {
return allRoomsLoadingStateFlow
}
override fun allRooms(): StateFlow<List<RoomSummary>> {
return allRoomSummariesFlow
}
override fun inviteRooms(): StateFlow<List<RoomSummary>> {
return inviteRoomSummariesFlow
} }
var latestSlidingSyncRange: IntRange? = null var latestSlidingSyncRange: IntRange? = null
private set private set
override fun setSlidingSyncRange(range: IntRange) { override fun updateRoomListVisibleRange(range: IntRange) {
latestSlidingSyncRange = range latestSlidingSyncRange = range
} }
} }

View file

@ -0,0 +1,43 @@
/*
* 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.sync
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSyncService : SyncService {
private val syncStateFlow = MutableStateFlow(SyncState.Idle)
fun simulateError() {
syncStateFlow.value = SyncState.InError
}
override fun startSync(): Result<Unit> {
syncStateFlow.value = SyncState.Syncing
return Result.success(Unit)
}
override fun stopSync(): Result<Unit> {
syncStateFlow.value = SyncState.Terminated
return Result.success(Unit)
}
override val syncState: StateFlow<SyncState> = syncStateFlow
}

View file

@ -31,7 +31,6 @@ class FakeMatrixTimeline(
private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState) private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems) private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
var isInitialized = false
fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) { fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) {
paginationState.value = update(paginationState.value) paginationState.value = update(paginationState.value)
@ -63,25 +62,6 @@ class FakeMatrixTimeline(
return Result.success(Unit) return Result.success(Unit)
} }
override fun initialize() {
isInitialized = true
}
override fun dispose() {
isInitialized = false
}
override suspend fun sendMessage(message: String): Result<Unit> {
return Result.success(Unit)
}
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
return Result.success(Unit)
}
override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
return Result.success(Unit)
}
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> { override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> {
return Result.success(Unit) return Result.success(Unit)

View file

@ -42,7 +42,7 @@ class MainActivity : ComponentActivity() {
RustMatrixAuthenticationService( RustMatrixAuthenticationService(
context = applicationContext, context = applicationContext,
baseDirectory = baseDirectory, baseDirectory = baseDirectory,
coroutineScope = Singleton.appScope, appCoroutineScope = Singleton.appScope,
coroutineDispatchers = Singleton.coroutineDispatchers, coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = InMemorySessionStore(), sessionStore = InMemorySessionStore(),
clock = DefaultSystemClock() clock = DefaultSystemClock()

View file

@ -69,7 +69,7 @@ class RoomListScreen(
stateContentFormatter = StateContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider),
), ),
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
networkMonitor = NetworkMonitorImpl(context), networkMonitor = NetworkMonitorImpl(context, Singleton.appScope),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers),
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers) leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers)
@ -81,13 +81,11 @@ class RoomListScreen(
Singleton.appScope.launch { Singleton.appScope.launch {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
matrixClient.getRoom(roomId)!!.use { room -> matrixClient.getRoom(roomId)!!.use { room ->
room.open()
val timeline = room.timeline() val timeline = room.timeline()
timeline.apply { timeline.apply {
// TODO This doesn't work reliably as initialize is asynchronous, and the timeline can't be used until it's finished // TODO This doesn't work reliably as initialize is asynchronous, and the timeline can't be used until it's finished
initialize()
paginateBackwards(20, 50) paginateBackwards(20, 50)
dispose()
} }
} }
} }
@ -109,10 +107,10 @@ class RoomListScreen(
DisposableEffect(Unit) { DisposableEffect(Unit) {
Timber.w("Start sync!") Timber.w("Start sync!")
matrixClient.startSync() matrixClient.syncService().startSync()
onDispose { onDispose {
Timber.w("Stop sync!") Timber.w("Stop sync!")
matrixClient.stopSync() matrixClient.syncService().stopSync()
} }
} }
} }