From a609433a5053c38d50e8b006408174db8b027fa0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 Oct 2022 20:17:18 +0200 Subject: [PATCH] Start implementing logic for room summary list --- .../java/io/element/android/x/MainActivity.kt | 6 + .../x/features/roomlist/RoomListScreen.kt | 30 ++-- .../x/features/roomlist/RoomListViewModel.kt | 51 ++----- .../x/features/roomlist/RoomListViewState.kt | 4 +- .../x/core/data/CoroutineDispatchers.kt | 9 ++ .../x/designsystem/components/Avatar.kt | 7 +- libraries/matrix/build.gradle | 1 + .../io/element/android/x/matrix/Matrix.kt | 22 ++- .../element/android/x/matrix/MatrixClient.kt | 133 ++++++++++------- .../element/android/x/matrix/core/EventId.kt | 4 + .../element/android/x/matrix/core/RoomId.kt | 4 + .../element/android/x/matrix/core/UserId.kt | 4 + .../android/x/matrix/room/MatrixRoom.kt | 11 ++ .../android/x/matrix/room/RoomSummary.kt | 26 ++++ .../x/matrix/room/RoomSummaryDataSource.kt | 139 ++++++++++++++++++ .../x/matrix/room/message/RoomMessage.kt | 11 ++ .../matrix/room/message/RoomMessageFactory.kt | 19 +++ .../matrix/{store => session}/SessionStore.kt | 4 +- .../x/matrix/sync/SlidingSyncViewFlows.kt | 32 ++++ .../android/x/matrix/util/CallbackFlow.kt | 15 ++ 20 files changed, 418 insertions(+), 114 deletions(-) create mode 100644 libraries/core/src/main/java/io/element/android/x/core/data/CoroutineDispatchers.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/core/EventId.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/core/RoomId.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/core/UserId.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessage.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessageFactory.kt rename libraries/matrix/src/main/java/io/element/android/x/matrix/{store => session}/SessionStore.kt (97%) create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncViewFlows.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/util/CallbackFlow.kt diff --git a/app/src/main/java/io/element/android/x/MainActivity.kt b/app/src/main/java/io/element/android/x/MainActivity.kt index 460fd533f3..f85e435ce5 100644 --- a/app/src/main/java/io/element/android/x/MainActivity.kt +++ b/app/src/main/java/io/element/android/x/MainActivity.kt @@ -1,10 +1,15 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.x import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.rememberNavHostEngine import io.element.android.x.designsystem.ElementXTheme @@ -32,6 +37,7 @@ private fun MainScreen(viewModel: MainViewModel) { val startRoute = runBlocking { if (!viewModel.hasSession()) LoginScreenNavigationDestination else NavGraphs.root.startRoute } + DestinationsNavHost( engine = engine, navController = navController, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt index a6ec323633..cc01c403d0 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt @@ -12,17 +12,19 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.airbnb.mvrx.Success import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel -import io.element.android.x.ui.theme.components.Avatar -import org.matrix.rustcomponents.sdk.Room +import io.element.android.x.designsystem.components.Avatar +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.room.RoomSummary @Composable fun RoomListScreen( viewModel: RoomListViewModel = mavericksViewModel(), - onRoomClicked: (String) -> Unit = { }, onLogoutClicked: () -> Unit = { }, + onRoomClicked: (RoomId) -> Unit = { } ) { val state by viewModel.collectAsState() RoomListContent(state, onRoomClicked, onLogoutClicked) @@ -31,8 +33,8 @@ fun RoomListScreen( @Composable fun RoomListContent( state: RoomListViewState, - onRoomClicked: (String) -> Unit, - onLogoutClicked: () -> Unit + onRoomClicked: (RoomId) -> Unit, + onLogoutClicked: () -> Unit, ) { Surface(color = MaterialTheme.colorScheme.background) { Column( @@ -62,7 +64,8 @@ fun RoomListTopBar(state: RoomListViewState, onLogoutClicked: () -> Unit) { TopAppBar( title = { Row( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically ) { val matrixUser = state.user Avatar(data = matrixUser.avatarData) @@ -83,19 +86,24 @@ fun RoomListTopBar(state: RoomListViewState, onLogoutClicked: () -> Unit) { @Composable private fun RoomItem( modifier: Modifier = Modifier, - room: Room, - onClick: (String) -> Unit + room: RoomSummary, + onClick: (RoomId) -> Unit ) { + if (room !is RoomSummary.Filled) { + return + } + val details = room.details Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier .clickable { - onClick(room.id()) + onClick(room.details.roomId) } .fillMaxWidth() + .padding(horizontal = 8.dp) ) { Column(modifier = modifier.padding(8.dp)) { - Text(text = "Room: ${room.name() ?: room.id()}") - Text(text = if (room.isDirect()) "Direct" else "Room") + Text(fontSize = 18.sp, text = details.name.orEmpty()) + Text(text = details.lastMessage?.toString().orEmpty(), maxLines = 2) } } } \ No newline at end of file diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt index 39c46a03f8..3a76373c3b 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt @@ -4,19 +4,14 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.Success -import io.element.android.x.core.data.tryOrNull import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixInstance import kotlinx.coroutines.launch -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.StoppableSpawn -import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.mediaSourceFromUrl class RoomListViewModel(initialState: RoomListViewState) : - MavericksViewModel(initialState), MatrixClient.SlidingSyncListener { + MavericksViewModel(initialState) { - private var sync: StoppableSpawn? = null private val matrix = MatrixInstance.getInstance() init { @@ -33,18 +28,26 @@ class RoomListViewModel(initialState: RoomListViewState) : private fun handleInit() { viewModelScope.launch { val client = getClient() - val url = client.avatarUrl() - val mediaSource = mediaSourceFromUrl(url) + client.startSync() + val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() + val userDisplayName = client.loadUserDisplayName().getOrNull() + val avatarData = userAvatarUrl?.let { + mediaSourceFromUrl(it) + }?.let { + client.loadMediaContentForSource(it) + } setState { copy( user = MatrixUser( - username = tryOrNull { client.username() } ?: "Room list", - avatarUrl = mediaSource.url(), - avatarData = client.loadMedia2(url) + username = userDisplayName, + avatarUrl = userAvatarUrl, + avatarData = avatarData?.getOrNull() ) ) } - sync = client.slidingSync(listener = this@RoomListViewModel) + client.roomSummaryDataSource().roomSummaries().execute { + copy(rooms = it) + } } } @@ -64,31 +67,7 @@ class RoomListViewModel(initialState: RoomListViewState) : return matrix.restoreSession()!! } - override fun onSyncUpdate( - summary: UpdateSummary, - rooms: List - ) = withState { state -> - val list = state.rooms().orEmpty().toMutableList() - rooms.forEach { room -> - // Either replace or add the room - val idx = list.indexOfFirst { it.id() == room.id() } - if (idx == -1) { - list.add(room) - } else { - list[idx] = room - } - } - - setState { - copy( - rooms = Success(list), - summary = Success(summary) - ) - } - } - override fun onCleared() { super.onCleared() - sync?.cancel() } } \ No newline at end of file diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewState.kt index 337f7eac77..1af2de24a5 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewState.kt @@ -3,13 +3,13 @@ package io.element.android.x.features.roomlist import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized +import io.element.android.x.matrix.room.RoomSummary import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.UpdateSummary data class RoomListViewState( val user: MatrixUser = MatrixUser(), - val rooms: Async> = Uninitialized, - val summary: Async = Uninitialized, + val rooms: Async> = Uninitialized, val canLoadMore: Boolean = false, val logoutAction: Async = Uninitialized, ) : MavericksState diff --git a/libraries/core/src/main/java/io/element/android/x/core/data/CoroutineDispatchers.kt b/libraries/core/src/main/java/io/element/android/x/core/data/CoroutineDispatchers.kt new file mode 100644 index 0000000000..cea96b5e43 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/data/CoroutineDispatchers.kt @@ -0,0 +1,9 @@ +package io.element.android.x.core.data + +import kotlinx.coroutines.CoroutineDispatcher + +data class CoroutineDispatchers( + val io: CoroutineDispatcher, + val computation: CoroutineDispatcher, + val main: CoroutineDispatcher, +) diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt index ab5f815bd2..c7cdebdf83 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/Avatar.kt @@ -1,4 +1,4 @@ -package io.element.android.x.ui.theme.components +package io.element.android.x.designsystem.components import android.util.Log import androidx.compose.foundation.Image @@ -9,6 +9,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter @@ -18,7 +19,7 @@ import coil.compose.rememberAsyncImagePainter @Composable fun Avatar( data: List?, - size: Int = 48, + size: Dp = 48.dp, ) { Image( painter = rememberAsyncImagePainter( @@ -28,7 +29,7 @@ fun Avatar( }), contentDescription = null, modifier = Modifier - .size(size.dp) + .size(size) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) diff --git a/libraries/matrix/build.gradle b/libraries/matrix/build.gradle index abe22c6b00..f86cc7a090 100644 --- a/libraries/matrix/build.gradle +++ b/libraries/matrix/build.gradle @@ -29,6 +29,7 @@ android { dependencies { api(name: 'matrix-rust-sdk', ext: 'aar') + implementation project(":libraries:core") implementation "net.java.dev.jna:jna:5.10.0@aar" implementation 'androidx.datastore:datastore-core:1.0.0' implementation 'androidx.datastore:datastore-preferences:1.0.0' diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt index f230a36185..459359484a 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt @@ -1,8 +1,10 @@ package io.element.android.x.matrix import android.content.Context -import io.element.android.x.matrix.store.SessionStore +import io.element.android.x.core.data.CoroutineDispatchers +import io.element.android.x.matrix.session.SessionStore import io.element.android.x.matrix.util.logError +import kotlinx.coroutines.Dispatchers import org.matrix.rustcomponents.sdk.AuthenticationService import org.matrix.rustcomponents.sdk.ClientBuilder import java.io.File @@ -10,6 +12,12 @@ import java.io.File class Matrix( context: Context, ) { + + private val coroutineDispatchers = CoroutineDispatchers( + io = Dispatchers.IO, + computation = Dispatchers.Default, + main = Dispatchers.Main + ) private val baseFolder = File(context.filesDir, "matrix") private val sessionStore = SessionStore(context) @@ -17,18 +25,18 @@ class Matrix( return sessionStore.getStoredData() ?.let { sessionData -> try { - val client = ClientBuilder() + ClientBuilder() .basePath(baseFolder.absolutePath) .username(sessionData.userId) - .build() - client.restoreLogin(sessionData.restoreToken) - client + .build().apply { + restoreLogin(sessionData.restoreToken) + } } catch (throwable: Throwable) { logError(throwable) null } }?.let { - MatrixClient(it, sessionStore) + MatrixClient(it, sessionStore, coroutineDispatchers) } } @@ -37,6 +45,6 @@ class Matrix( authService.configureHomeserver(homeserver) val client = authService.login(username, password, "MatrixRustSDKSample", null) sessionStore.storeData(SessionStore.SessionData(client.userId(), client.restoreToken())) - return MatrixClient(client, sessionStore) + return MatrixClient(client, sessionStore, coroutineDispatchers) } } \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt index 7225da6dda..ae22b7f00e 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt @@ -1,78 +1,105 @@ package io.element.android.x.matrix import android.util.Log -import io.element.android.x.matrix.store.SessionStore +import io.element.android.x.core.data.CoroutineDispatchers +import io.element.android.x.matrix.core.UserId +import io.element.android.x.matrix.room.RoomSummaryDataSource +import io.element.android.x.matrix.room.RustRoomSummaryDataSource +import io.element.android.x.matrix.session.SessionStore +import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.* +import java.io.Closeable class MatrixClient internal constructor( private val client: Client, private val sessionStore: SessionStore, -) { - private val roomWrapper = RoomWrapper(client) + private val dispatchers: CoroutineDispatchers, +) : Closeable { - fun startSync() { - val clientDelegate = object : ClientDelegate { - override fun didReceiveAuthError(isSoftLogout: Boolean) { - Log.v(LOG_TAG, "didReceiveAuthError()") - } + private val clientDelegate = object : ClientDelegate { + override fun didReceiveAuthError(isSoftLogout: Boolean) { + Log.v(LOG_TAG, "didReceiveAuthError()") + } - override fun didReceiveSyncUpdate() { - Log.v(LOG_TAG, "didReceiveSyncUpdate()") - } + override fun didReceiveSyncUpdate() { + Log.v(LOG_TAG, "didReceiveSyncUpdate()") + } - override fun didUpdateRestoreToken() { - Log.v(LOG_TAG, "didUpdateRestoreToken()") - } + override fun didUpdateRestoreToken() { + Log.v(LOG_TAG, "didUpdateRestoreToken()") } + } - client.setDelegate(clientDelegate) - Log.v(LOG_TAG, "DisplayName = ${client.displayName()}") - try { - client.fullSlidingSync() - } catch (failure: Throwable) { - Log.e(LOG_TAG, "fullSlidingSync() fail", failure) + private val slidingSyncObserver = object : SlidingSyncObserver { + override fun didReceiveSyncUpdate(summary: UpdateSummary) { + Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary") + roomSummaryDataSource.updateRoomsWithIdentifiers(summary.rooms) } } - fun slidingSync(listener: SlidingSyncListener): StoppableSpawn { - val slidingSyncView = SlidingSyncViewBuilder() - .timelineLimit(limit = 10u) - .requiredState(requiredState = listOf(RequiredState(key = "m.room.avatar", value = ""))) - .name(name = "HomeScreenView") - .syncMode(mode = SlidingSyncMode.FULL_SYNC) - .build() - - val slidingSync = client - .slidingSync() - .homeserver("https://slidingsync.lab.element.dev") - .addView(slidingSyncView) - .build() - - slidingSync.setObserver(object : SlidingSyncObserver { - override fun didReceiveSyncUpdate(summary: UpdateSummary) { - Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary") - val rooms = summary.rooms.mapNotNull { - roomWrapper.getRoom(it) - } - listener.onSyncUpdate(summary, rooms) - } - }) - return slidingSync.sync() + private val slidingSyncView = SlidingSyncViewBuilder() + .timelineLimit(limit = 10u) + .requiredState(requiredState = listOf(RequiredState(key = "m.room.avatar", value = ""))) + .name(name = "HomeScreenView") + .syncMode(mode = SlidingSyncMode.FULL_SYNC) + .build() + + private val slidingSync = client + .slidingSync() + .homeserver("https://slidingsync.lab.element.dev") + .addView(slidingSyncView) + .build() + + private val roomSummaryDataSource: RustRoomSummaryDataSource = + RustRoomSummaryDataSource(slidingSync, slidingSyncView, dispatchers) + private var slidingSyncObserverToken: StoppableSpawn? = null + + init { + client.setDelegate(clientDelegate) + } + + fun startSync() { + slidingSync.setObserver(slidingSyncObserver) + slidingSyncObserverToken = slidingSync.sync() + } + + fun stopSync() { + slidingSync.setObserver(null) + slidingSyncObserverToken?.cancel() + } + + fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource + + override fun close() { + stopSync() + client.setDelegate(null) } - suspend fun logout() { + suspend fun logout() = withContext(dispatchers.io) { + close() client.logout() sessionStore.reset() } - fun userId(): String = client.userId() - fun username(): String = client.displayName() - fun avatarUrl(): String = client.avatarUrl() - - fun loadMedia(source: MediaSource) = client.getMediaContent(source) - fun loadMedia2(mxcUrl: String) = client.getMediaContent(mediaSourceFromUrl(mxcUrl)) + fun userId(): UserId = UserId(client.userId()) + suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { + runCatching { + client.displayName() + } + } - interface SlidingSyncListener { - fun onSyncUpdate(summary: UpdateSummary, rooms: List) + suspend fun loadUserAvatarURLString(): Result = withContext(dispatchers.io) { + runCatching { + client.avatarUrl() + } } + + suspend fun loadMediaContentForSource(source: MediaSource): Result> = + withContext(dispatchers.io) { + runCatching { + client.getMediaContent(source) + } + } + + } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/core/EventId.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/EventId.kt new file mode 100644 index 0000000000..55afdd934a --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/EventId.kt @@ -0,0 +1,4 @@ +package io.element.android.x.matrix.core + +@JvmInline +value class EventId(val value: String) \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/core/RoomId.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/RoomId.kt new file mode 100644 index 0000000000..48ea0ccfde --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/RoomId.kt @@ -0,0 +1,4 @@ +package io.element.android.x.matrix.core + +@JvmInline +value class RoomId(val value: String) \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/core/UserId.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/UserId.kt new file mode 100644 index 0000000000..4e8b8cf858 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/UserId.kt @@ -0,0 +1,4 @@ +package io.element.android.x.matrix.core + +@JvmInline +value class UserId(val value: String) \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt new file mode 100644 index 0000000000..ba564e1bc2 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -0,0 +1,11 @@ +package io.element.android.x.matrix.room + +import io.element.android.x.matrix.core.RoomId +import org.matrix.rustcomponents.sdk.Room + +class MatrixRoom(private val room: Room) { + + val roomId = RoomId(room.id()) + + +} \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt new file mode 100644 index 0000000000..55cb2d03cb --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt @@ -0,0 +1,26 @@ +package io.element.android.x.matrix.room + +import io.element.android.x.matrix.core.RoomId + +sealed interface RoomSummary { + data class Empty(val identifier: String) : RoomSummary + data class Filled(val details: RoomSummaryDetails) : RoomSummary + + fun identifier(): String { + return when (this) { + is Empty -> identifier + is Filled -> details.roomId.value + } + } + +} + +data class RoomSummaryDetails( + val roomId: RoomId, + val name: String?, + val isDirect: Boolean, + val avatarURLString: String?, + val lastMessage: CharSequence?, + val lastMessageTimestamp: Long?, + val unreadNotificationCount: UInt, +) diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt new file mode 100644 index 0000000000..a6cfa88970 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt @@ -0,0 +1,139 @@ +package io.element.android.x.matrix.room + +import io.element.android.x.core.data.CoroutineDispatchers +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.room.message.RoomMessageFactory +import io.element.android.x.matrix.sync.roomListDiff +import io.element.android.x.matrix.sync.state +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.matrix.rustcomponents.sdk.* +import java.util.* + +interface RoomSummaryDataSource { + fun roomSummaries(): Flow> +} + +internal class RustRoomSummaryDataSource( + private val slidingSync: SlidingSync, + private val slidingSyncView: SlidingSyncView, + private val coroutineDispatchers: CoroutineDispatchers, + private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(), +) : RoomSummaryDataSource { + + private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) + + private val roomSummaries = MutableStateFlow>(emptyList()) + private val state = MutableStateFlow(SlidingSyncState.COLD) + + init { + slidingSyncView.roomListDiff() + .onEach { diff -> + updateRoomSummaries { + applyDiff(diff) + } + }.launchIn(coroutineScope) + + slidingSyncView.state() + .onEach { newRoomState -> + state.value = newRoomState + }.launchIn(coroutineScope) + } + + override fun roomSummaries(): Flow> { + return roomSummaries + } + + internal fun updateRoomsWithIdentifiers(identifiers: List) { + if (state.value != SlidingSyncState.LIVE) { + return + } + val roomSummaryList = roomSummaries.value.toMutableList() + for (identifier in identifiers) { + val index = roomSummaryList.indexOfFirst { it.identifier() == identifier } + if (index == -1) { + continue + } + val updatedRoomSummary = buildRoomSummaryForIdentifier(identifier) + roomSummaryList[index] = updatedRoomSummary + } + roomSummaries.value = roomSummaryList + } + + private fun MutableList.applyDiff(diff: SlidingSyncViewRoomsListDiff) { + if (diff.isInvalidation()) { + return + } + when (diff) { + is SlidingSyncViewRoomsListDiff.Push -> { + val roomSummary = buildSummaryForRoomListEntry(diff.value) + add(roomSummary) + } + is SlidingSyncViewRoomsListDiff.UpdateAt -> { + val roomSummary = buildSummaryForRoomListEntry(diff.value) + set(diff.index.toInt(), roomSummary) + } + is SlidingSyncViewRoomsListDiff.InsertAt -> { + val roomSummary = buildSummaryForRoomListEntry(diff.value) + add(diff.index.toInt(), roomSummary) + } + is SlidingSyncViewRoomsListDiff.Move -> { + Collections.swap(this, diff.oldIndex.toInt(), diff.newIndex.toInt()) + } + is SlidingSyncViewRoomsListDiff.RemoveAt -> { + removeAt(diff.index.toInt()) + } + is SlidingSyncViewRoomsListDiff.Replace -> { + clear() + addAll(diff.values.map { buildSummaryForRoomListEntry(it) }) + } + } + } + + private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { + return when (entry) { + RoomListEntry.Empty -> RoomSummary.Empty(UUID.randomUUID().toString()) + is RoomListEntry.Invalidated -> buildRoomSummaryForIdentifier(entry.roomId) + is RoomListEntry.Filled -> buildRoomSummaryForIdentifier(entry.roomId) + } + } + + private fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary { + val room = slidingSync.getRoom(identifier) ?: return RoomSummary.Empty(identifier) + val latestRoomMessage = room.latestRoomMessage()?.let { + roomMessageFactory.create(it) + } + return RoomSummary.Filled( + details = RoomSummaryDetails( + roomId = RoomId(identifier), + name = room.name(), + isDirect = room.isDm() ?: false, + avatarURLString = room.fullRoom()?.avatarUrl(), + unreadNotificationCount = room.unreadNotifications().notificationCount(), + lastMessage = latestRoomMessage?.body, + lastMessageTimestamp = latestRoomMessage?.originServerTs + ) + ) + } + + private fun updateRoomSummaries(block: MutableList.() -> Unit) { + val mutableRoomSummaries = roomSummaries.value.toMutableList() + block(mutableRoomSummaries) + roomSummaries.value = mutableRoomSummaries + } + +} + +fun SlidingSyncViewRoomsListDiff.isInvalidation(): Boolean { + return when (this) { + is SlidingSyncViewRoomsListDiff.InsertAt -> this.value is RoomListEntry.Invalidated + is SlidingSyncViewRoomsListDiff.UpdateAt -> this.value is RoomListEntry.Invalidated + is SlidingSyncViewRoomsListDiff.Push -> this.value is RoomListEntry.Invalidated + else -> false + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessage.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessage.kt new file mode 100644 index 0000000000..5986dc349c --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessage.kt @@ -0,0 +1,11 @@ +package io.element.android.x.matrix.room.message + +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.core.UserId + +data class RoomMessage( + val eventId: EventId, + val body: String, + val sender: UserId, + val originServerTs: Long, +) diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessageFactory.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessageFactory.kt new file mode 100644 index 0000000000..dd0890a91d --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/message/RoomMessageFactory.kt @@ -0,0 +1,19 @@ +package io.element.android.x.matrix.room.message + +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.core.UserId +import org.matrix.rustcomponents.sdk.AnyMessage + +class RoomMessageFactory { + + fun create(anyMessage: AnyMessage): RoomMessage? { + val textMessage = anyMessage.textMessage()?.baseMessage() ?: return null + return RoomMessage( + eventId = EventId(textMessage.id()), + body = textMessage.body(), + sender = UserId(textMessage.sender()), + originServerTs = textMessage.originServerTs().toLong() + ) + } + +} \ No newline at end of file diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/store/SessionStore.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/session/SessionStore.kt similarity index 97% rename from libraries/matrix/src/main/java/io/element/android/x/matrix/store/SessionStore.kt rename to libraries/matrix/src/main/java/io/element/android/x/matrix/session/SessionStore.kt index f483256c08..ff39c6ce3e 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/store/SessionStore.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/session/SessionStore.kt @@ -1,4 +1,4 @@ -package io.element.android.x.matrix.store +package io.element.android.x.matrix.session import android.content.Context import androidx.datastore.core.DataStore @@ -8,13 +8,13 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.firstOrNull - private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_sessions") private val userIdPreference = stringPreferencesKey("userId") // TODO It contains the access token, so it has to be stored in a more secured storage. // I would expect the Rust SDK to provide a more obscure token. private val restoreTokenPreference = stringPreferencesKey("restoreToken") + internal class SessionStore( context: Context ) { diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncViewFlows.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncViewFlows.kt new file mode 100644 index 0000000000..f9b4aeeb76 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncViewFlows.kt @@ -0,0 +1,32 @@ +package io.element.android.x.matrix.sync + +import kotlinx.coroutines.flow.Flow +import mxCallbackFlow +import org.matrix.rustcomponents.sdk.* + +fun SlidingSyncView.roomListDiff(): Flow = mxCallbackFlow { + val observer = object : SlidingSyncViewRoomListObserver { + override fun didReceiveUpdate(diff: SlidingSyncViewRoomsListDiff) { + trySend(diff) + } + } + observeRoomList(observer) +} + +fun SlidingSyncView.state(): Flow = mxCallbackFlow { + val observer = object : SlidingSyncViewStateObserver { + override fun didReceiveUpdate(newState: SlidingSyncState) { + trySend(newState) + } + } + observeState(observer) +} + +fun SlidingSyncView.roomsCount(): Flow = mxCallbackFlow { + val observer = object : SlidingSyncViewRoomsCountObserver { + override fun didReceiveUpdate(count: UInt) { + trySend(count) + } + } + observeRoomsCount(observer) +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/util/CallbackFlow.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/util/CallbackFlow.kt new file mode 100644 index 0000000000..23bb5f6fbc --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/util/CallbackFlow.kt @@ -0,0 +1,15 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import org.matrix.rustcomponents.sdk.StoppableSpawn + +internal fun mxCallbackFlow(block: suspend ProducerScope.() -> StoppableSpawn) = + callbackFlow { + val token: StoppableSpawn = block(this) + awaitClose { + token.cancel() + } + } \ No newline at end of file