From 11b74c02791c96eb81b763e2a4db565152c5f6d8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 4 Nov 2022 17:56:14 +0100 Subject: [PATCH] Start filling MatrixRoom class and use it in MessagesScreen --- .../x/features/messages/MessagesScreen.kt | 21 +++++-- .../x/features/messages/MessagesViewModel.kt | 61 ++++++++++++------- .../messages/model/MessagesViewState.kt | 5 +- .../x/features/roomlist/RoomListViewModel.kt | 2 +- .../io/element/android/x/matrix/Matrix.kt | 11 +++- .../element/android/x/matrix/MatrixClient.kt | 44 ++++++++----- .../android/x/matrix/room/MatrixRoom.kt | 41 ++++++++++++- .../x/matrix/room/RoomSummaryDataSource.kt | 40 +++++------- .../matrix/room/RoomSummaryDetailsFactory.kt | 30 +++++++++ .../matrix/room/message/RoomMessageFactory.kt | 1 - .../x/matrix/sync/SlidingSyncObserverProxy.kt | 20 ++++++ 11 files changed, 202 insertions(+), 74 deletions(-) create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDetailsFactory.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncObserverProxy.kt diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt index 51b4169e56..372fbde825 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesScreen.kt @@ -2,6 +2,7 @@ package io.element.android.x.features.messages +import Avatar import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material3.* @@ -12,18 +13,23 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import io.element.android.x.core.data.LogCompositions +import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.features.messages.model.MessagesViewState @Composable fun MessagesScreen(roomId: String) { val viewModel: MessagesViewModel = mavericksViewModel(argsFactory = { roomId }) LogCompositions(tag = "MessagesScreen", msg = "Root") - val roomTitle by viewModel.collectAsState(prop1 = MessagesViewState::roomTitle) - MessagesContent(roomTitle) + val roomTitle by viewModel.collectAsState(MessagesViewState::roomName) + val roomAvatar by viewModel.collectAsState(MessagesViewState::roomAvatar) + MessagesContent(roomTitle, roomAvatar) } @Composable -fun MessagesContent(roomTitle: String) { +fun MessagesContent( + roomTitle: String?, + roomAvatar: AvatarData? +) { val appBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState) LogCompositions(tag = "RoomListScreen", msg = "Content") @@ -31,7 +37,14 @@ fun MessagesContent(roomTitle: String) { modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { TopAppBar( - title = { Text(text = roomTitle) } + navigationIcon = { + if (roomAvatar != null) { + IconButton(onClick = {}) { + Avatar(roomAvatar) + } + } + }, + title = { Text(text = roomTitle ?: "") } ) }, content = { padding -> diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt index f0d5a877c2..c8760cb98b 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt @@ -1,48 +1,69 @@ package io.element.android.x.features.messages -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.parallelMap +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.ViewModelContext import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.features.messages.model.MessagesViewState import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixInstance -import io.element.android.x.matrix.room.RoomSummary -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import io.element.android.x.matrix.room.MatrixRoom +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.rustcomponents.sdk.mediaSourceFromUrl -class MessagesViewModel(initialState: MessagesViewState) : +class MessagesViewModel( + private val client: MatrixClient, + private val room: MatrixRoom, + private val initialState: MessagesViewState +) : MavericksViewModel(initialState) { - private val matrix = MatrixInstance.getInstance() + companion object : MavericksViewModelFactory { + + override fun create( + viewModelContext: ViewModelContext, + state: MessagesViewState + ): MessagesViewModel? { + val matrix = MatrixInstance.getInstance() + val client = matrix.activeClient() + val room = client.getRoom(state.roomId) ?: return null + return MessagesViewModel(client, room, state) + } + + } + init { handleInit() } private fun handleInit() { - viewModelScope.launch { - - - } + room.syncUpdateFlow() + .onEach { + val avatarData = + loadAvatarData(room.name ?: room.roomId.value, room.avatarUrl, AvatarSize.SMALL) + setState { + copy( + roomName = room.name, roomAvatar = avatarData, + ) + } + }.launchIn(viewModelScope) } private suspend fun loadAvatarData( - client: MatrixClient, name: String, url: String?, size: AvatarSize = AvatarSize.MEDIUM ): AvatarData { val mediaContent = url?.let { val mediaSource = mediaSourceFromUrl(it) - client.loadMediaThumbnailForSource(mediaSource, size.value.toLong(), size.value.toLong()) + client.loadMediaThumbnailForSource( + mediaSource, + size.value.toLong(), + size.value.toLong() + ) } return mediaContent?.fold( { it }, @@ -52,10 +73,6 @@ class MessagesViewModel(initialState: MessagesViewState) : } } - private suspend fun getClient(): MatrixClient { - return matrix.matrixClient().first().get() - } - override fun onCleared() { super.onCleared() } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt index d3656966f5..0ccc161395 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/model/MessagesViewState.kt @@ -2,15 +2,14 @@ package io.element.android.x.features.messages.model import com.airbnb.mvrx.MavericksState import io.element.android.x.designsystem.components.avatar.AvatarData -import io.element.android.x.matrix.core.RoomId data class MessagesViewState( val roomId: String, - val roomTitle: String = "", + val roomName: String? = null, val roomAvatar: AvatarData? = null ) : MavericksState { @Suppress("unused") - constructor(roomId: String) : this(roomId = roomId, roomTitle = "", roomAvatar = null) + constructor(roomId: String) : this(roomId = roomId, roomName = null, roomAvatar = null) } 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 1fd8728c72..806a0c4d7b 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 @@ -131,7 +131,7 @@ class RoomListViewModel(initialState: RoomListViewState) : } private suspend fun getClient(): MatrixClient { - return matrix.matrixClient().first().get() + return matrix.client().first().get() } override fun onCleared() { 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 a2c743cd96..31e4591b70 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 @@ -12,10 +12,10 @@ import org.matrix.rustcomponents.sdk.AuthenticationService import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder import java.io.File -import java.util.Optional +import java.util.* class Matrix( - coroutineScope: CoroutineScope, + private val coroutineScope: CoroutineScope, context: Context, ) { private val coroutineDispatchers = CoroutineDispatchers( @@ -44,10 +44,14 @@ class Matrix( return isLoggedIn } - fun matrixClient(): Flow> { + fun client(): Flow> { return matrixClient } + fun activeClient(): MatrixClient { + return matrixClient.value.get() + } + suspend fun restoreSession() = withContext(coroutineDispatchers.io) { sessionStore.getStoredData() ?.let { sessionData -> @@ -80,6 +84,7 @@ class Matrix( return MatrixClient( client = client, sessionStore = sessionStore, + coroutineScope = coroutineScope, dispatchers = coroutineDispatchers ).also { matrixClient.value = Optional.of(it) 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 f974b322a1..983acdd655 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 @@ -2,9 +2,14 @@ package io.element.android.x.matrix import io.element.android.x.core.data.CoroutineDispatchers import io.element.android.x.matrix.core.UserId +import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.RoomSummaryDataSource +import io.element.android.x.matrix.room.RoomSummaryDetailsFactory import io.element.android.x.matrix.room.RustRoomSummaryDataSource +import io.element.android.x.matrix.room.message.RoomMessageFactory import io.element.android.x.matrix.session.SessionStore +import io.element.android.x.matrix.sync.SlidingSyncObserverProxy +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.* import timber.log.Timber @@ -13,6 +18,7 @@ import java.io.Closeable class MatrixClient internal constructor( private val client: Client, private val sessionStore: SessionStore, + private val coroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, ) : Closeable { @@ -30,19 +36,15 @@ class MatrixClient internal constructor( } } - private val slidingSyncObserver = object : SlidingSyncObserver { - override fun didReceiveSyncUpdate(summary: UpdateSummary) { - Timber.v("didReceiveSyncUpdate=$summary on Thread: ${Thread.currentThread()}") - roomSummaryDataSource.updateRoomsWithIdentifiers(summary.rooms) - } - } - private val slidingSyncView = SlidingSyncViewBuilder() .timelineLimit(limit = 1u) - .requiredState(requiredState = listOf( - RequiredState(key = "m.room.avatar", value = ""), - RequiredState(key = "m.room.encryption", value = ""), - )) + .requiredState( + requiredState = listOf( + RequiredState(key = "m.room.avatar", value = ""), + RequiredState(key = "m.room.name", value = ""), + RequiredState(key = "m.room.encryption", value = ""), + ) + ) .name(name = "HomeScreenView") .syncMode(mode = SlidingSyncMode.FULL_SYNC) .build() @@ -54,15 +56,28 @@ class MatrixClient internal constructor( .addView(slidingSyncView) .build() + private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope) private val roomSummaryDataSource: RustRoomSummaryDataSource = - RustRoomSummaryDataSource(slidingSync, slidingSyncView, dispatchers) + RustRoomSummaryDataSource(slidingSyncObserverProxy.updateSummaryFlow, slidingSync, slidingSyncView, dispatchers) private var slidingSyncObserverToken: StoppableSpawn? = null init { client.setDelegate(clientDelegate) } + + fun getRoom(roomId: String): MatrixRoom? { + val slidingSyncRoom = slidingSync.getRoom(roomId) ?: return null + val room = slidingSyncRoom.fullRoom() ?: return null + return MatrixRoom( + slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, + slidingSyncRoom = slidingSyncRoom, + room = room + ) + } + fun startSync() { - slidingSync.setObserver(slidingSyncObserver) + roomSummaryDataSource.startSync() + slidingSync.setObserver(slidingSyncObserverProxy) slidingSyncObserverToken = slidingSync.sync() } @@ -113,7 +128,8 @@ class MatrixClient internal constructor( ): Result = withContext(dispatchers.io) { runCatching { - client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray().toByteArray() + client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray() + .toByteArray() } } 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 index ba564e1bc2..60f510c9e6 100644 --- 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 @@ -1,11 +1,50 @@ package io.element.android.x.matrix.room import io.element.android.x.matrix.core.RoomId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSyncRoom +import org.matrix.rustcomponents.sdk.UpdateSummary -class MatrixRoom(private val room: Room) { +class MatrixRoom( + private val slidingSyncUpdateFlow: Flow, + private val slidingSyncRoom: SlidingSyncRoom, + private val room: Room, +) { + + fun syncUpdateFlow(): Flow { + return slidingSyncUpdateFlow + .filter { + it.rooms.contains(room.id()) + } + .map { } + .onStart { emit(Unit) } + } val roomId = RoomId(room.id()) + val name: String? + get() { + return slidingSyncRoom.name() + } + + val displayName: String + get() { + return room.displayName() + } + + val topic: String? + get() { + return room.topic() + } + + val avatarUrl: String? + get() { + return room.avatarUrl() + } + } \ No newline at end of file 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 index 3319a03ea4..138d53a3a9 100644 --- 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 @@ -1,7 +1,6 @@ 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 @@ -20,10 +19,11 @@ interface RoomSummaryDataSource { } internal class RustRoomSummaryDataSource( + private val slidingSyncUpdateFlow: Flow, private val slidingSync: SlidingSync, private val slidingSyncView: SlidingSyncView, private val coroutineDispatchers: CoroutineDispatchers, - private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(), + private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory() ) : RoomSummaryDataSource, Closeable { private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) @@ -31,7 +31,8 @@ internal class RustRoomSummaryDataSource( private val roomSummaries = MutableStateFlow>(emptyList()) private val state = MutableStateFlow(SlidingSyncState.COLD) - init { + + fun startSync(){ slidingSyncView.roomListDiff() .onEach { diff -> updateRoomSummaries { @@ -44,6 +45,11 @@ internal class RustRoomSummaryDataSource( Timber.v("New sliding sync state: $slidingSyncState") state.value = slidingSyncState }.launchIn(coroutineScope) + + slidingSyncUpdateFlow + .onEach { + didReceiveSyncUpdate(it) + }.launchIn(coroutineScope) } fun stopSync() { @@ -58,13 +64,13 @@ internal class RustRoomSummaryDataSource( return roomSummaries.sample(100) } - internal fun updateRoomsWithIdentifiers(identifiers: List) { - Timber.v("UpdateRooms with identifiers: $identifiers") + private fun didReceiveSyncUpdate(summary: UpdateSummary) { + Timber.v("UpdateRooms with identifiers: ${summary.rooms}") if (state.value != SlidingSyncState.LIVE) { return } updateRoomSummaries { - for (identifier in identifiers) { + for (identifier in summary.rooms) { val index = indexOfFirst { it.identifier() == identifier } if (index == -1) { continue @@ -78,7 +84,7 @@ internal class RustRoomSummaryDataSource( private fun MutableList.applyDiff(diff: SlidingSyncViewRoomsListDiff) { fun MutableList.fillUntil(untilIndex: Int) { - repeat((size-1 until untilIndex).count()) { + repeat((size - 1 until untilIndex).count()) { add(buildEmptyRoomSummary()) } } @@ -89,7 +95,7 @@ internal class RustRoomSummaryDataSource( add(roomSummary) } is SlidingSyncViewRoomsListDiff.UpdateAt -> { - fillUntil(diff.index.toInt()) + //fillUntil(diff.index.toInt()) val roomSummary = buildSummaryForRoomListEntry(diff.value) set(diff.index.toInt(), roomSummary) } @@ -124,24 +130,8 @@ internal class RustRoomSummaryDataSource( private fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary { val room = slidingSync.getRoom(identifier) ?: return RoomSummary.Empty(identifier) - val latestRoomMessage = room.latestRoomMessage()?.let { - roomMessageFactory.create(it) - } - val computedLastMessage = when { - latestRoomMessage == null -> null - room.isDm() == true -> latestRoomMessage.body - else -> "${latestRoomMessage.sender.value}: ${latestRoomMessage.body}" - } return RoomSummary.Filled( - details = RoomSummaryDetails( - roomId = RoomId(identifier), - name = room.name() ?: identifier, - isDirect = room.isDm() ?: false, - avatarURLString = room.fullRoom()?.avatarUrl(), - unreadNotificationCount = room.unreadNotifications().notificationCount().toInt(), - lastMessage = computedLastMessage, - lastMessageTimestamp = latestRoomMessage?.originServerTs - ) + details = roomSummaryDetailsFactory.create(room, room.fullRoom()) ) } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDetailsFactory.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDetailsFactory.kt new file mode 100644 index 0000000000..0427987bd5 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDetailsFactory.kt @@ -0,0 +1,30 @@ +package io.element.android.x.matrix.room + +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.room.message.RoomMessageFactory +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSyncRoom + +class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) { + + fun create(slidingSyncRoom: SlidingSyncRoom, room: Room?): RoomSummaryDetails{ + val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.let { + roomMessageFactory.create(it) + } + val computedLastMessage = when { + latestRoomMessage == null -> null + slidingSyncRoom.isDm() == true -> latestRoomMessage.body + else -> "${latestRoomMessage.sender.value}: ${latestRoomMessage.body}" + } + return RoomSummaryDetails( + roomId = RoomId(slidingSyncRoom.roomId()), + name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(), + isDirect = slidingSyncRoom.isDm() ?: false, + avatarURLString = room?.avatarUrl(), + unreadNotificationCount = slidingSyncRoom.unreadNotifications().notificationCount().toInt(), + lastMessage = computedLastMessage, + lastMessageTimestamp = latestRoomMessage?.originServerTs + ) + } + +} \ No newline at end of file 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 index 5929c84b4b..7c82c42472 100644 --- 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 @@ -5,7 +5,6 @@ import io.element.android.x.matrix.core.UserId import org.matrix.rustcomponents.sdk.EventTimelineItem class RoomMessageFactory { - fun create(eventTimelineItem: EventTimelineItem?): RoomMessage? { eventTimelineItem ?: return null return RoomMessage( diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncObserverProxy.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncObserverProxy.kt new file mode 100644 index 0000000000..bac8e9cf1c --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/sync/SlidingSyncObserverProxy.kt @@ -0,0 +1,20 @@ +package io.element.android.x.matrix.sync + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.matrix.rustcomponents.sdk.SlidingSyncObserver +import org.matrix.rustcomponents.sdk.UpdateSummary + +class SlidingSyncObserverProxy(private val coroutineScope: CoroutineScope) : SlidingSyncObserver { + + private val updateSummaryMutableFlow = MutableSharedFlow() + val updateSummaryFlow: Flow = updateSummaryMutableFlow + + override fun didReceiveSyncUpdate(summary: UpdateSummary) { + coroutineScope.launch { + updateSummaryMutableFlow.emit(summary) + } + } +} \ No newline at end of file