From 5104fc8ac1aa260b85da86653d9d16f4247e8cea Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 Apr 2023 15:58:20 +0200 Subject: [PATCH 01/33] Fix bottomsheet not using right theme (BottomSheet is not part of material3) --- .../features/messages/impl/actionlist/ActionListView.kt | 9 ++++++++- .../theme/components/ModalBottomSheetLayout.kt | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index dc71d1d834..c2becf71cf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -102,6 +102,7 @@ private fun SheetContent( // Crashes if sheetContent size is zero Box(modifier = modifier.size(1.dp)) } + is ActionListState.Target.Success -> { val actions = target.actions LazyColumn( @@ -146,5 +147,11 @@ fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) st @Composable private fun ContentToPreview(state: ActionListState) { - SheetContent(state) + ActionListView( + state = state, + modalBottomSheetState = ModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded + ), + onActionSelected = { _, _ -> } + ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt index 7c498a610c..d897f6ad4e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt @@ -21,12 +21,12 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetDefaults import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.contentColorFor import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,7 +44,7 @@ fun ModalBottomSheetLayout( sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), sheetShape: Shape = MaterialTheme.shapes.large, sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, - sheetBackgroundColor: Color = MaterialTheme.colors.surface, + sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface, sheetContentColor: Color = contentColorFor(sheetBackgroundColor), scrimColor: Color = ModalBottomSheetDefaults.scrimColor, content: @Composable () -> Unit = {} From a1869a30192ef23c94ea14cf18c0622bfe8b377c Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 Apr 2023 18:21:47 +0200 Subject: [PATCH 02/33] Improve handling members --- .../io/element/android/appnav/RoomFlowNode.kt | 7 ++- .../android/appnav/RoomFlowNodeTest.kt | 14 ----- .../impl/root/CreateRoomRootPresenterTests.kt | 2 +- .../event/TimelineItemEventFactory.kt | 2 + .../roomdetails/impl/RoomDetailsPresenter.kt | 27 +++++----- .../roomdetails/impl/di/RoomMemberModules.kt | 9 ---- .../impl/members/RoomMemberListNode.kt | 1 + .../impl/members/RoomUserListDataSource.kt | 2 +- .../roomdetails/RoomDetailsPresenterTests.kt | 25 +++++---- .../libraries/matrix/api/room/MatrixRoom.kt | 39 ++++++++++---- .../libraries/matrix/impl/RustMatrixClient.kt | 2 +- .../matrix/impl/room/RustMatrixRoom.kt | 47 ++++------------- .../impl/timeline/RustMatrixTimeline.kt | 16 +++++- .../libraries/matrix/test/FakeMatrixClient.kt | 2 +- .../matrix/test/room/FakeMatrixRoom.kt | 51 ++++++------------- 15 files changed, 107 insertions(+), 139 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 22929e3d66..4eaa5b83df 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -41,7 +41,6 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -102,12 +101,18 @@ class RoomFlowNode @AssistedInject constructor( private fun fetchRoomMembers() = lifecycleScope.launch { val room = inputs.room + /* room.fetchMembers() + .map { + room.updateMembers() + } .onFailure { Timber.e(it, "Fail to fetch members for room ${room.roomId}") }.onSuccess { Timber.v("Success fetching members for room ${room.roomId}") } + + */ } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index ef611a2f4b..a151be665c 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -24,7 +24,6 @@ import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.activeElement import com.bumble.appyx.testing.junit4.util.MainDispatcherRule -import com.bumble.appyx.testing.unit.common.helper.nodeTestHelper import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.google.common.truth.Truth import io.element.android.features.messages.api.MessagesEntryPoint @@ -81,19 +80,6 @@ class RoomFlowNodeTest { roomMembershipObserver = RoomMembershipObserver() ) - @Test - fun `given a room flow node when initialized then it fetches room members`() { - // GIVEN - val room = FakeMatrixRoom() - val inputs = RoomFlowNode.Inputs(room) - val roomFlowNode = aRoomFlowNode(listOf(inputs)) - Truth.assertThat(room.areMembersFetched).isFalse() - // WHEN - roomFlowNode.nodeTestHelper() - // THEN - Truth.assertThat(room.areMembersFetched).isTrue() - } - @Test fun `given a room flow node when initialized then it loads messages entry point`() { // GIVEN diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 64d59b221c..db7cd06170 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -102,7 +102,7 @@ class CreateRoomRootPresenterTests { }.test { val initialState = awaitItem() val matrixUser = MatrixUser(UserId("@name:domain")) - val fakeDmResult = FakeMatrixRoom(RoomId("!fakeDmResult:domain")) + val fakeDmResult = FakeMatrixRoom(roomId = RoomId("!fakeDmResult:domain")) fakeMatrixClient.givenFindDmResult(fakeDmResult) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index c21622b50f..c62719ae51 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber import javax.inject.Inject class TimelineItemEventFactory @Inject constructor( @@ -42,6 +43,7 @@ class TimelineItemEventFactory @Inject constructor( val senderDisplayName: String? val senderAvatarUrl: String? + Timber.v("SenderProfile($currentSender) = ${currentTimelineItem.event.senderProfile}") when (val senderProfile = currentTimelineItem.event.senderProfile) { ProfileTimelineDetails.Unavailable, ProfileTimelineDetails.Pending, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 45e978b80a..969e2aa86b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,17 +26,16 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.architecture.executeResult 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.getDmMember +import io.element.android.libraries.matrix.api.room.memberCount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( - private val sessionId: SessionId, private val room: MatrixRoom, private val roomMembershipObserver: RoomMembershipObserver, ) : Presenter { @@ -50,15 +50,14 @@ class RoomDetailsPresenter @Inject constructor( mutableStateOf(null) } - var memberCount: Async by remember { mutableStateOf(Async.Loading()) } + val memberCount: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - memberCount = runCatching { room.memberCount() } - .fold( - onSuccess = { Async.Success(it) }, - onFailure = { Async.Failure(it) } - ) - } + suspend { + room.updateMembers() + .map { room.memberCount() } + }.executeResult(memberCount) } val dmMember = room.getDmMember() @@ -72,7 +71,7 @@ class RoomDetailsPresenter @Inject constructor( when (event) { is RoomDetailsEvent.LeaveRoom -> { if (event.needsConfirmation) { - leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount) + leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount.value) } else { coroutineScope.launch(Dispatchers.IO) { room.leave() @@ -96,7 +95,7 @@ class RoomDetailsPresenter @Inject constructor( roomAlias = room.alias, roomAvatarUrl = room.avatarUrl, roomTopic = room.topic, - memberCount = memberCount, + memberCount = memberCount.value, isEncrypted = room.isEncrypted, displayLeaveRoomWarning = leaveRoomWarning, error = error, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt index 68f77b5821..aeb42893e8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt @@ -44,15 +44,6 @@ interface RoomMemberBindsModule { @ContributesTo(RoomScope::class) object RoomMemberProvidesModule { - @Provides - fun provideRoomDetailsPresenter( - matrixClient: MatrixClient, - room: MatrixRoom, - roomMembershipObserver: RoomMembershipObserver, - ): RoomDetailsPresenter { - return RoomDetailsPresenter(matrixClient.sessionId, room, roomMembershipObserver) - } - @Provides fun provideRoomMemberDetailsPresenterFactory( room: MatrixRoom, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index fafe0ede99..60ba07d1ee 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -28,6 +28,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.getMember import io.element.android.libraries.matrix.ui.model.MatrixUser import timber.log.Timber diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index dc0008d2a3..6bf1203e83 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -30,7 +30,7 @@ class RoomUserListDataSource @Inject constructor( ) : UserListDataSource { override suspend fun search(query: String): List { - return room.members().filter { member -> + return room.membersFlow.value.filter { member -> if (query.isBlank()) { true } else { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 66e74cd5cc..1d25a27e44 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,7 +49,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -59,7 +58,7 @@ class RoomDetailsPresenterTests { Truth.assertThat(initialState.roomName).isEqualTo(room.name) Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl) Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic) - Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null)) + Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) cancelAndIgnoreRemainingEvents() @@ -69,12 +68,12 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null)) + Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) val finalState = awaitItem() Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) @@ -84,7 +83,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -100,7 +99,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom(name = null).apply { givenFetchMemberResult(Result.failure(Throwable())) } - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -114,7 +113,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { val room = aMatrixRoom(isPublic = false) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -131,7 +130,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { val room = aMatrixRoom(members = listOf(aRoomMember())) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -148,7 +147,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation shows a generic warning`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -165,7 +164,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave without confirmation leaves the room`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -189,7 +188,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenLeaveRoomError(Throwable()) } - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -218,10 +217,10 @@ fun aMatrixRoom( ) = FakeMatrixRoom( roomId = roomId, name = name, + initialMembers = members, displayName = displayName, topic = topic, avatarUrl = avatarUrl, - members = members, isEncrypted = isEncrypted, isPublic = isPublic, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index d4338bb497..fae6550a4f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -18,12 +18,15 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import java.io.Closeable -interface MatrixRoom: Closeable { +interface MatrixRoom : Closeable { + val sessionId: SessionId val roomId: RoomId val name: String? val bestName: String @@ -36,20 +39,22 @@ interface MatrixRoom: Closeable { val isDirect: Boolean val isPublic: Boolean - suspend fun members() : List + /** + * The current loaded members as a StateFlow. + * Initial value is an emptyList. + * To update them you should call [updateMembers]. + */ + val membersFlow: StateFlow> - suspend fun memberCount(): Int - - fun getMember(userId: UserId): RoomMember? - - fun getDmMember(): RoomMember? + /** + * Try to load the room members and update the membersFlow. + */ + suspend fun updateMembers(): Result fun syncUpdateFlow(): Flow fun timeline(): MatrixTimeline - suspend fun fetchMembers(): Result - suspend fun userDisplayName(userId: UserId): Result suspend fun userAvatarUrl(userId: UserId): Result @@ -64,3 +69,19 @@ interface MatrixRoom: Closeable { suspend fun leave(): Result } + +fun MatrixRoom.getMember(userId: UserId): RoomMember? { + return membersFlow.value.find { it.userId == userId } +} + +fun MatrixRoom.getDmMember(): RoomMember? { + return if (membersFlow.value.size == 2 && isDirect && isEncrypted) { + membersFlow.value.find { it.userId != this.sessionId } + } else { + null + } +} + +fun MatrixRoom.memberCount(): Int { + return membersFlow.value.size +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 63bd4e13a2..2a7e7c0826 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -198,7 +198,7 @@ class RustMatrixClient constructor( val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null val fullRoom = slidingSyncRoom.fullRoom() ?: return null return RustMatrixRoom( - currentUserId = sessionId, + sessionId = sessionId, slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, slidingSyncRoom = slidingSyncRoom, innerRoom = fullRoom, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index cc0eac2e1f..1b87d271dc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -17,21 +17,21 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.SlidingSyncRoom @@ -40,7 +40,7 @@ import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown class RustMatrixRoom( - private val currentUserId: UserId, + override val sessionId: SessionId, private val slidingSyncUpdateFlow: Flow, private val slidingSyncRoom: SlidingSyncRoom, private val innerRoom: Room, @@ -48,39 +48,10 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { - private var loadMembersJob: Job? = null - private var cachedMembers: List = emptyList() + override val membersFlow: StateFlow> + get() = cachedMembers - override suspend fun members(): List { - return cachedMembers.ifEmpty { - if (loadMembersJob == null) { - loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) { - cachedMembers = tryOrNull { - innerRoom.members().map(RoomMemberMapper::map) - } ?: emptyList() - } - } - loadMembersJob?.join() - loadMembersJob = null - cachedMembers - } - } - - override suspend fun memberCount(): Int { - return members().size - } - - override fun getMember(userId: UserId): RoomMember? { - return cachedMembers.find { it.userId == userId } - } - - override fun getDmMember(): RoomMember? { - return if (cachedMembers.size == 2 && isDirect && isEncrypted) { - cachedMembers.find { it.userId != currentUserId } - } else { - null - } - } + private var cachedMembers = MutableStateFlow>(emptyList()) override fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow @@ -150,9 +121,9 @@ class RustMatrixRoom( override val isDirect: Boolean get() = innerRoom.isDirect() - override suspend fun fetchMembers(): Result = withContext(coroutineDispatchers.io) { + override suspend fun updateMembers(): Result = withContext(coroutineDispatchers.io) { runCatching { - innerRoom.fetchMembers() + cachedMembers.value = innerRoom.members().map(RoomMemberMapper::map) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index e942f31b76..17e865f9fb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -139,10 +139,24 @@ class RustMatrixTimeline( private suspend fun addListener(timelineListener: TimelineListener): Result> = withContext(coroutineDispatchers.io) { runCatching { - val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null) + val settings = RoomSubscription( + requiredState = listOf( + RequiredState(key = "m.room.canonical_alias", value = ""), + RequiredState(key = "m.room.topic", value = ""), + RequiredState(key = "m.room.join_rule", value = ""), + ), + timelineLimit = 20.toUInt() + ) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) + fetchMembers() listenerTokens += result.taskHandle result.items } } + + private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.fetchMembers() + } + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index c2a234e3de..2c08023f97 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -54,7 +54,7 @@ class FakeMatrixClient( private var logoutFailure: Throwable? = null override fun getRoom(roomId: RoomId): MatrixRoom? { - return FakeMatrixRoom(roomId) + return FakeMatrixRoom(sessionId = sessionId, roomId = roomId) } override fun findDM(userId: UserId): MatrixRoom? { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 902f41a80c..b80b84a666 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -18,17 +18,21 @@ package io.element.android.libraries.matrix.test.room import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow class FakeMatrixRoom( + override val sessionId: SessionId = A_SESSION_ID, override val roomId: RoomId = A_ROOM_ID, override val name: String? = null, override val bestName: String = "", @@ -40,20 +44,23 @@ class FakeMatrixRoom( override val alternativeAliases: List = emptyList(), override val isPublic: Boolean = true, override val isDirect: Boolean = false, - private val members: List = emptyList(), + initialMembers: List = emptyList(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { private var userDisplayNameResult = Result.success(null) private var userAvatarUrlResult = Result.success(null) private var dmMember: RoomMember? = null - private var fetchMemberResult: Result = Result.success(Unit) - - var areMembersFetched: Boolean = false - private set + private var updateMembersResult: Result = Result.success(Unit) private var leaveRoomError: Throwable? = null + override val membersFlow: MutableStateFlow> = MutableStateFlow(initialMembers) + + override suspend fun updateMembers(): Result { + return updateMembersResult + } + override fun syncUpdateFlow(): Flow { return emptyFlow() } @@ -62,18 +69,6 @@ class FakeMatrixRoom( return matrixTimeline } - override suspend fun fetchMembers(): Result { - return fetchMemberResult.also { result -> - if (result.isSuccess) { - areMembersFetched = true - } - } - } - - override fun getDmMember(): RoomMember? { - return dmMember - } - override suspend fun userDisplayName(userId: UserId): Result { return userDisplayNameResult } @@ -82,22 +77,6 @@ class FakeMatrixRoom( return userAvatarUrlResult } - override suspend fun members(): List { - return members - } - - override suspend fun memberCount(): Int { - if (fetchMemberResult.isSuccess) { - return members.count() - } else { - throw fetchMemberResult.exceptionOrNull()!! - } - } - - override fun getMember(userId: UserId): RoomMember? { - return members.firstOrNull { it.userId == userId } - } - override suspend fun sendMessage(message: String): Result { delay(100) return Result.success(Unit) @@ -139,7 +118,7 @@ class FakeMatrixRoom( } fun givenFetchMemberResult(result: Result) { - fetchMemberResult = result + updateMembersResult = result } fun givenDmMember(roomMember: RoomMember) { From 90bfe9725061fdf5731f3b508b0d1203a94ac250 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 21 Apr 2023 14:39:47 +0200 Subject: [PATCH 03/33] Room : continue improving members loading --- .../io/element/android/appnav/RoomFlowNode.kt | 8 +- features/messages/impl/build.gradle.kts | 1 + .../messages/impl/timeline/TimelineView.kt | 26 ++-- .../event/TimelineItemEventFactory.kt | 2 - .../messages/MessagesPresenterTest.kt | 1 - .../messages/fixtures/timelineItemsFactory.kt | 4 + features/roomdetails/impl/build.gradle.kts | 1 + .../roomdetails/impl/RoomDetailsPresenter.kt | 115 +++++++++++------- .../impl/members/RoomMemberListEvents.kt} | 15 +-- .../impl/members/RoomMemberListNode.kt | 14 +-- .../impl/members/RoomMemberListPresenter.kt | 32 ++++- .../impl/members/RoomMemberListState.kt | 4 +- .../members/RoomMemberListStateProvider.kt | 1 + .../impl/members/RoomMemberListView.kt | 17 ++- .../impl/members/RoomUserListDataSource.kt | 29 +++-- .../roomdetails/RoomDetailsPresenterTests.kt | 60 ++++----- .../members/RoomMemberListPresenterTests.kt | 15 ++- .../userlist/api/UserListDataSource.kt | 1 + .../android/libraries/architecture/Async.kt | 4 +- .../libraries/matrix/api/room/MatrixRoom.kt | 30 +++-- .../matrix/api/room/MatrixRoomMembersState.kt | 31 +++++ .../matrix/impl/room/RustMatrixRoom.kt | 17 ++- .../impl/timeline/RustMatrixTimeline.kt | 5 +- .../matrix/test/room/FakeMatrixRoom.kt | 14 +-- tests/testutils/build.gradle.kts | 2 + .../testutils/TestCoroutineDispatchers.kt | 46 +++++++ 26 files changed, 329 insertions(+), 166 deletions(-) rename features/{messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt => roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt} (56%) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 4eaa5b83df..7cd33c2958 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -101,18 +101,12 @@ class RoomFlowNode @AssistedInject constructor( private fun fetchRoomMembers() = lifecycleScope.launch { val room = inputs.room - /* - room.fetchMembers() - .map { - room.updateMembers() - } + room.updateMembers() .onFailure { Timber.e(it, "Fail to fetch members for room ${room.roomId}") }.onSuccess { Timber.v("Success fetching members for room ${room.roomId}") } - - */ } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 9b43d41d4d..b695cc5512 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.tests.testutils) androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 7ea427a8cf..c7b071e069 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -75,6 +75,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import timber.log.Timber @Composable fun TimelineView( @@ -100,11 +101,11 @@ fun TimelineView( itemsIndexed( items = state.timelineItems, contentType = { _, timelineItem -> timelineItem.contentType() }, - key = { _, timelineItem -> timelineItem.key() }, + key = { _, timelineItem -> timelineItem.identifier() }, ) { index, timelineItem -> TimelineItemRow( timelineItem = timelineItem, - isHighlighted = timelineItem.key() == state.highlightedEventId?.value, + isHighlighted = timelineItem.identifier() == state.highlightedEventId?.value, onClick = onMessageClicked, onLongClick = onMessageLongClicked ) @@ -114,27 +115,22 @@ fun TimelineView( } } + /* TimelineScrollHelper( lazyListState = lazyListState, timelineItems = state.timelineItems, onLoadMore = ::onReachedLoadMore ) + + */ } } -private fun TimelineItem.key(): String { - return when (this) { - is TimelineItem.Event -> id - is TimelineItem.Virtual -> id - } -} - -private fun TimelineItem.contentType(): Int { - // Todo optimize for each subtype - return when (this) { - is TimelineItem.Event -> 0 - is TimelineItem.Virtual -> 1 - } +private fun TimelineItem.contentType() = when (this) { + is TimelineItem.Event -> content.javaClass.simpleName + is TimelineItem.Virtual -> model.javaClass.simpleName +}.also { + Timber.v("ContentType = $it") } @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index c62719ae51..c21622b50f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -25,7 +25,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber import javax.inject.Inject class TimelineItemEventFactory @Inject constructor( @@ -43,7 +42,6 @@ class TimelineItemEventFactory @Inject constructor( val senderDisplayName: String? val senderAvatarUrl: String? - Timber.v("SenderProfile($currentSender) = ${currentTimelineItem.event.senderProfile}") when (val senderProfile = currentTimelineItem.event.senderProfile) { ProfileTimelineDetails.Unavailable, ProfileTimelineDetails.Pending, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index a7bc112174..bc898d5159 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -131,7 +131,6 @@ class MessagesPresenterTest { appCoroutineScope = this, room = matrixRoom ) - val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt index 404e50a3bd..2d4ee3842c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages.fixtures import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory @@ -31,6 +33,8 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi internal fun aTimelineItemsFactory() = TimelineItemsFactory( dispatchers = testCoroutineDispatchers(), diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index dc840b038f..41f117d6a4 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 969e2aa86b..9b6a1916c7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,75 +17,60 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.executeResult +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.room.getDmMember -import io.element.android.libraries.matrix.api.room.memberCount -import kotlinx.coroutines.Dispatchers +import io.element.android.libraries.matrix.api.room.getDmMemberFlow +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( private val room: MatrixRoom, private val roomMembershipObserver: RoomMembershipObserver, + private val coroutineDispatchers: CoroutineDispatchers, ) : Presenter { @Composable override fun present(): RoomDetailsState { val coroutineScope = rememberCoroutineScope() - var leaveRoomWarning by remember { + val leaveRoomWarning = remember { mutableStateOf(null) } - var error by remember { + val error = remember { mutableStateOf(null) } + val membersState by room.membersStateFlow.collectAsState() + val memberCount by getMemberCount(membersState) + val dmMemberState by room.getDmMemberFlow() + .collectAsState(initial = null, context = coroutineDispatchers.computation) - val memberCount: MutableState> = remember { - mutableStateOf(Async.Uninitialized) - } - LaunchedEffect(Unit) { - suspend { - room.updateMembers() - .map { room.memberCount() } - }.executeResult(memberCount) - } - - val dmMember = room.getDmMember() - val roomType = if (dmMember != null) { - RoomDetailsType.Dm(dmMember) - } else { - RoomDetailsType.Room - } + val roomType = getRoomType(dmMemberState) fun handleEvents(event: RoomDetailsEvent) { when (event) { is RoomDetailsEvent.LeaveRoom -> { - if (event.needsConfirmation) { - leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount.value) - } else { - coroutineScope.launch(Dispatchers.IO) { - room.leave() - .onSuccess { - roomMembershipObserver.notifyUserLeftRoom(room.roomId) - }.onFailure { - error = RoomDetailsError.AlertGeneric - } - leaveRoomWarning = null - } - } + coroutineScope.leaveRoom( + needsConfirmation = event.needsConfirmation, + memberCount = memberCount, + leaveRoomWarning = leaveRoomWarning, + error = error, + ) } - is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null - RoomDetailsEvent.ClearError -> error = null + is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning.value = null + RoomDetailsEvent.ClearError -> error.value = null } } @@ -95,12 +80,56 @@ class RoomDetailsPresenter @Inject constructor( roomAlias = room.alias, roomAvatarUrl = room.avatarUrl, roomTopic = room.topic, - memberCount = memberCount.value, + memberCount = memberCount, isEncrypted = room.isEncrypted, - displayLeaveRoomWarning = leaveRoomWarning, - error = error, - roomType = roomType, + displayLeaveRoomWarning = leaveRoomWarning.value, + error = error.value, + roomType = roomType.value, eventSink = ::handleEvents, ) } + + @Composable + private fun getRoomType(dmMember: RoomMember?): State = remember(dmMember) { + derivedStateOf { + if (dmMember != null) { + RoomDetailsType.Dm(dmMember) + } else { + RoomDetailsType.Room + } + } + } + + @Composable + private fun getMemberCount(membersState: MatrixRoomMembersState): State> = remember(membersState) { + derivedStateOf { + when (membersState) { + MatrixRoomMembersState.Unknown -> Async.Uninitialized + MatrixRoomMembersState.Pending -> Async.Loading() + is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size) + is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure) + } + } + } + + private fun CoroutineScope.leaveRoom( + needsConfirmation: Boolean, + memberCount: Async, + leaveRoomWarning: MutableState, + error: MutableState, + ) = launch(coroutineDispatchers.io) { + if (needsConfirmation) { + leaveRoomWarning.value = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount) + } else { + room.leave() + .onSuccess { + roomMembershipObserver.notifyUserLeftRoom(room.roomId) + }.onFailure { + error.value = RoomDetailsError.AlertGeneric + } + leaveRoomWarning.value = null + } + } } + + diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt similarity index 56% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt index 60b0b7cf4b..4534842cac 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListEvents.kt @@ -14,15 +14,10 @@ * limitations under the License. */ -package io.element.android.features.messages.fixtures +package io.element.android.features.roomdetails.impl.members -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import io.element.android.libraries.matrix.ui.model.MatrixUser -// TODO Move to common module to reuse -internal fun testCoroutineDispatchers() = CoroutineDispatchers( - io = UnconfinedTestDispatcher(), - computation = UnconfinedTestDispatcher(), - main = UnconfinedTestDispatcher(), - diffUpdateDispatcher = UnconfinedTestDispatcher(), -) +sealed interface RoomMemberListEvents { + data class SelectUser(val user: MatrixUser) : RoomMemberListEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 60ba07d1ee..ff2b1a0245 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -28,9 +28,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.getMember -import io.element.android.libraries.matrix.ui.model.MatrixUser -import timber.log.Timber @ContributesNode(RoomScope::class) class RoomMemberListNode @AssistedInject constructor( @@ -46,12 +43,9 @@ class RoomMemberListNode @AssistedInject constructor( private val callbacks = plugins() - private fun onUserSelected(matrixUser: MatrixUser) { - val member = room.getMember(matrixUser.id) - if (member != null) { - callbacks.forEach { it.openRoomMemberDetails(member) } - } else { - Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}") + private fun openRoomMemberDetails(roomMember: RoomMember) { + callbacks.forEach { + it.openRoomMemberDetails(roomMember) } } @@ -62,7 +56,7 @@ class RoomMemberListNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = { navigateUp() }, - onUserSelected = ::onUserSelected, + onMemberSelected = this::openRoomMemberDetails, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 8841bd9d5e..897e56728b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -18,8 +18,10 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListDataStore @@ -27,10 +29,16 @@ import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.getMemberFlow import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Named @@ -39,6 +47,8 @@ class RoomMemberListPresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, @Named("RoomMembers") private val userListDataSource: UserListDataSource, private val userListDataStore: UserListDataStore, + private val room: MatrixRoom, + private val coroutineDispatchers: CoroutineDispatchers, ) : Presenter { private val userListPresenter by lazy { @@ -51,17 +61,33 @@ class RoomMemberListPresenter @Inject constructor( @Composable override fun present(): RoomMemberListState { + val coroutineScope = rememberCoroutineScope() val userListState = userListPresenter.present() val allUsers = remember { mutableStateOf>>(Async.Loading()) } + val selectedMember: MutableState = remember { + mutableStateOf(null) + } LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { allUsers.value = Async.Success(userListDataSource.search("").toImmutableList()) } } + + fun handleEvents(roomMemberListEvents: RoomMemberListEvents) { + when (roomMemberListEvents) { + is RoomMemberListEvents.SelectUser -> coroutineScope.loadRoomMember(roomMemberListEvents.user, selectedMember) + } + } return RoomMemberListState( allUsers = allUsers.value, - userListState = userListState + userListState = userListState, + selectedRoomMember = selectedMember.value, + eventSink = ::handleEvents ) } + + private fun CoroutineScope.loadRoomMember(user: MatrixUser, selectedMember: MutableState) = launch(coroutineDispatchers.io) { + selectedMember.value = room.getMemberFlow(user.id).firstOrNull() + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index f5e5bd3efb..42289b9ebe 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -18,11 +18,13 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.features.userlist.api.UserListState import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList data class RoomMemberListState( val allUsers: Async>, val userListState: UserListState, -// val eventSink: (AddPeopleEvents) -> Unit, + val selectedRoomMember: RoomMember? = null, + val eventSink: (RoomMemberListEvents) -> Unit, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index fc98ae7544..57012c6d86 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -39,4 +39,5 @@ internal fun aRoomMemberListState( RoomMemberListState( userListState = aUserListState().copy(searchResults = searchResults), allUsers = allUsers, + eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index f356e203f2..47aeb635df 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource @@ -51,6 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser @OptIn(ExperimentalMaterial3Api::class) @@ -59,8 +61,19 @@ fun RoomMemberListView( state: RoomMemberListState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onUserSelected: (MatrixUser) -> Unit = {}, + onMemberSelected: (RoomMember) -> Unit = {}, ) { + + LaunchedEffect(state.selectedRoomMember) { + if (state.selectedRoomMember != null) { + onMemberSelected(state.selectedRoomMember) + } + } + + fun onUserSelected(user: MatrixUser) { + state.eventSink(RoomMemberListEvents.SelectUser(user)) + } + Scaffold( topBar = { if (!state.userListState.isSearchActive) { @@ -76,7 +89,7 @@ fun RoomMemberListView( ) { UserListView( state = state.userListState, - onUserSelected = onUserSelected, + onUserSelected = ::onUserSelected, ) if (!state.userListState.isSearchActive) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index 6bf1203e83..10d9d55c02 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -18,26 +18,40 @@ package io.element.android.features.roomdetails.impl.members import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.skip +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.withContext import javax.inject.Inject class RoomUserListDataSource @Inject constructor( - private val room: MatrixRoom + private val room: MatrixRoom, + private val coroutineDispatchers: CoroutineDispatchers, ) : UserListDataSource { - override suspend fun search(query: String): List { - return room.membersFlow.value.filter { member -> - if (query.isBlank()) { - true - } else { + override suspend fun search(query: String): List = withContext(coroutineDispatchers.io) { + val roomMembers = room.membersStateFlow + .dropWhile { it !is MatrixRoomMembersState.Ready} + .first() + .roomMembers() + val filteredMembers = if (query.isBlank()) { + roomMembers + } else { + roomMembers.filter { member -> member.userId.value.contains(query, ignoreCase = true) || member.displayName?.contains(query, ignoreCase = true).orFalse() } - }.map(::mapMemberToMatrixUser) + } + filteredMembers.map(::mapMemberToMatrixUser) } override suspend fun getProfile(userId: UserId): MatrixUser? { @@ -55,5 +69,4 @@ class RoomUserListDataSource @Inject constructor( ) ) } - } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 1d25a27e44..05617ebf1a 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -26,6 +26,7 @@ import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -45,11 +47,12 @@ import org.junit.Test class RoomDetailsPresenterTests { private val roomMembershipObserver = RoomMembershipObserver() + private val testCoroutineDispatchers = testCoroutineDispatchers() @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -68,13 +71,13 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) - + room.givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) val finalState = awaitItem() Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) } @@ -83,7 +86,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -97,30 +100,27 @@ class RoomDetailsPresenterTests { @Test fun `present - can handle error while fetching member count`() = runTest { val room = aMatrixRoom(name = null).apply { - givenFetchMemberResult(Result.failure(Throwable())) + givenRoomMembersState(MatrixRoomMembersState.Error(Throwable())) } - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - skipItems(1) Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java) - cancelAndIgnoreRemainingEvents() } } @Test fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { - val room = aMatrixRoom(isPublic = false) - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom(isPublic = false).apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom) @@ -129,15 +129,14 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { - val room = aMatrixRoom(members = listOf(aRoomMember())) - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(aRoomMember()))) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom) @@ -146,15 +145,14 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation shows a generic warning`() = runTest { - val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) val confirmationState = awaitItem() Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic) @@ -163,15 +161,14 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave without confirmation leaves the room`() = runTest { - val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val room = aMatrixRoom().apply { + givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) + } + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) cancelAndIgnoreRemainingEvents() @@ -188,14 +185,11 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenLeaveRoomError(Throwable()) } - val presenter = RoomDetailsPresenter(room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - // Allow room member count to load - skipItems(1) - initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) val errorState = awaitItem() Truth.assertThat(errorState.error).isNotNull() @@ -211,13 +205,11 @@ fun aMatrixRoom( displayName: String = "A fallback display name", topic: String? = "A topic", avatarUrl: String? = "https://matrix.org/avatar.jpg", - members: List = emptyList(), isEncrypted: Boolean = true, isPublic: Boolean = true, ) = FakeMatrixRoom( roomId = roomId, name = name, - initialMembers = members, displayName = displayName, topic = topic, avatarUrl = avatarUrl, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 373ebbb347..a5798381c0 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -29,8 +29,12 @@ import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.impl.DefaultUserListPresenter import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import okhttp3.internal.toImmutableList import org.junit.Test @@ -38,6 +42,8 @@ import org.junit.Test @ExperimentalCoroutinesApi class RoomMemberListPresenterTests { + private val testCoroutineDispatchers = testCoroutineDispatchers() + @Test fun `present - search is done automatically on start, but is async`() = runTest { val searchResult = listOf(aMatrixUser()) @@ -52,7 +58,14 @@ class RoomMemberListPresenterTests { userListDataStore: UserListDataStore, ) = DefaultUserListPresenter(args, userListDataSource, userListDataStore) } - val presenter = RoomMemberListPresenter(userListFactory, userListDataSource, userListDataStore) + val fakeRoom = FakeMatrixRoom() + val presenter = RoomMemberListPresenter( + userListPresenterFactory = userListFactory, + userListDataSource = userListDataSource, + userListDataStore = userListDataStore, + room = fakeRoom, + coroutineDispatchers = testCoroutineDispatchers + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt index afe2d1ab3d..2cfd23eb61 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser interface UserListDataSource { + //TODO should probably have a flow suspend fun search(query: String): List suspend fun getProfile(userId: UserId): MatrixUser? } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index bb74dff2a9..3be961598d 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -47,7 +47,9 @@ suspend fun (suspend () -> T).execute(state: MutableState>, errorMa } suspend fun (suspend () -> Result).executeResult(state: MutableState>) { - state.value = Async.Loading() + if (state.value !is Async.Success) { + state.value = Async.Loading() + } this().fold( onSuccess = { state.value = Async.Success(it) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index fae6550a4f..2bff8fe76c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import java.io.Closeable interface MatrixRoom : Closeable { @@ -41,10 +42,10 @@ interface MatrixRoom : Closeable { /** * The current loaded members as a StateFlow. - * Initial value is an emptyList. + * Initial value is [MatrixRoomMembersState.Unknown]. * To update them you should call [updateMembers]. */ - val membersFlow: StateFlow> + val membersStateFlow: StateFlow /** * Try to load the room members and update the membersFlow. @@ -70,18 +71,21 @@ interface MatrixRoom : Closeable { suspend fun leave(): Result } -fun MatrixRoom.getMember(userId: UserId): RoomMember? { - return membersFlow.value.find { it.userId == userId } -} - -fun MatrixRoom.getDmMember(): RoomMember? { - return if (membersFlow.value.size == 2 && isDirect && isEncrypted) { - membersFlow.value.find { it.userId != this.sessionId } - } else { - null +fun MatrixRoom.getMemberFlow(userId: UserId): Flow { + return membersStateFlow.map { state -> + state.roomMembers().find { + it.userId == userId + } } } -fun MatrixRoom.memberCount(): Int { - return membersFlow.value.size +fun MatrixRoom.getDmMemberFlow(): Flow { + return membersStateFlow.map { state -> + val members = state.roomMembers() + if (members.size == 2 && isDirect && isEncrypted) { + members.find { it.userId != this.sessionId } + } else { + null + } + } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt new file mode 100644 index 0000000000..319a5f7605 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -0,0 +1,31 @@ +/* + * 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.room + +sealed interface MatrixRoomMembersState { + object Unknown : MatrixRoomMembersState + object Pending : MatrixRoomMembersState + data class Error(val failure: Throwable) : MatrixRoomMembersState + data class Ready(val roomMembers: List) : MatrixRoomMembersState +} + +fun MatrixRoomMembersState.roomMembers(): List { + return when (this) { + is MatrixRoomMembersState.Ready -> roomMembers + else -> emptyList() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 1b87d271dc..9e780e57de 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope @@ -48,10 +48,10 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { - override val membersFlow: StateFlow> - get() = cachedMembers + override val membersStateFlow: StateFlow + get() = _membersStateFlow - private var cachedMembers = MutableStateFlow>(emptyList()) + private var _membersStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow @@ -122,9 +122,14 @@ class RustMatrixRoom( get() = innerRoom.isDirect() override suspend fun updateMembers(): Result = withContext(coroutineDispatchers.io) { + _membersStateFlow.value = MatrixRoomMembersState.Pending runCatching { - cachedMembers.value = innerRoom.members().map(RoomMemberMapper::map) - } + innerRoom.members().map(RoomMemberMapper::map) + }.onSuccess { + _membersStateFlow.value = MatrixRoomMembersState.Ready(it) + }.onFailure { + _membersStateFlow.value = MatrixRoomMembersState.Error(it) + }.map { } } override suspend fun userDisplayName(userId: UserId): Result = diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 17e865f9fb..6d19e817a4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -143,12 +143,15 @@ class RustMatrixTimeline( requiredState = listOf( RequiredState(key = "m.room.canonical_alias", value = ""), RequiredState(key = "m.room.topic", value = ""), + RequiredState(key = "m.room.name", value = ""), RequiredState(key = "m.room.join_rule", value = ""), ), timelineLimit = 20.toUInt() ) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) - fetchMembers() + launch { + fetchMembers() + } listenerTokens += result.taskHandle result.items } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index b80b84a666..a685e369d1 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -44,18 +44,16 @@ class FakeMatrixRoom( override val alternativeAliases: List = emptyList(), override val isPublic: Boolean = true, override val isDirect: Boolean = false, - initialMembers: List = emptyList(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { private var userDisplayNameResult = Result.success(null) private var userAvatarUrlResult = Result.success(null) - private var dmMember: RoomMember? = null private var updateMembersResult: Result = Result.success(Unit) private var leaveRoomError: Throwable? = null - override val membersFlow: MutableStateFlow> = MutableStateFlow(initialMembers) + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override suspend fun updateMembers(): Result { return updateMembersResult @@ -117,12 +115,12 @@ class FakeMatrixRoom( this.leaveRoomError = throwable } - fun givenFetchMemberResult(result: Result) { - updateMembersResult = result + fun givenRoomMembersState(state: MatrixRoomMembersState) { + membersStateFlow.value = state } - fun givenDmMember(roomMember: RoomMember) { - this.dmMember = roomMember + fun givenUpdateMembersResult(result: Result) { + updateMembersResult = result } fun givenUserDisplayNameResult(displayName: Result) { diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index 44167f906f..fda44f46d1 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -34,4 +34,6 @@ dependencies { implementation(libs.coroutines.test) implementation(projects.libraries.matrix.test) implementation(projects.services.appnavstate.test) + implementation(projects.services.appnavstate.test) + implementation(projects.libraries.core) } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt new file mode 100644 index 0000000000..1309a14cb1 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt @@ -0,0 +1,46 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.tests.testutils + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +fun testCoroutineDispatchers( + testScheduler: TestCoroutineScheduler? = null, +) = CoroutineDispatchers( + io = UnconfinedTestDispatcher(testScheduler), + computation = UnconfinedTestDispatcher(testScheduler), + main = UnconfinedTestDispatcher(testScheduler), + diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler), +) + +fun testCoroutineDispatchers( + io: TestDispatcher = UnconfinedTestDispatcher(), + computation: TestDispatcher = UnconfinedTestDispatcher(), + main: TestDispatcher = UnconfinedTestDispatcher(), + diffUpdateDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) = CoroutineDispatchers( + io = io, + computation = computation, + main = main, + diffUpdateDispatcher = diffUpdateDispatcher, +) From 860777d20c81df49d41eca337c609005699263c6 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 24 Apr 2023 00:09:54 +0000 Subject: [PATCH 04/33] Sync Strings from Localazy --- .../src/main/res/values-de/translations.xml | 7 ++ .../src/main/res/values-ro/translations.xml | 10 ++ .../src/main/res/values-de/translations.xml | 6 ++ .../src/main/res/values-es/translations.xml | 4 + .../src/main/res/values-it/translations.xml | 4 + .../src/main/res/values-ro/translations.xml | 9 ++ .../src/main/res/values-de/translations.xml | 7 ++ .../src/main/res/values-es/translations.xml | 8 +- .../src/main/res/values-it/translations.xml | 8 +- .../src/main/res/values-ro/translations.xml | 8 +- .../impl/src/main/res/values/localazy.xml | 8 +- .../src/main/res/values-de/translations.xml | 7 ++ .../src/main/res/values-de/translations.xml | 4 + .../src/main/res/values-de/translations.xml | 6 ++ .../src/main/res/values-de/translations.xml | 15 +++ .../src/main/res/values-es/translations.xml | 10 +- .../src/main/res/values-it/translations.xml | 10 +- .../src/main/res/values-ro/translations.xml | 8 +- .../impl/src/main/res/values/localazy.xml | 8 +- .../src/main/res/values-de/translations.xml | 40 +++++++ .../src/main/res/values-de/translations.xml | 10 ++ .../src/main/res/values-es/translations.xml | 4 +- .../src/main/res/values-it/translations.xml | 4 +- .../src/main/res/values-ro/translations.xml | 4 +- .../impl/src/main/res/values/localazy.xml | 4 +- .../src/main/res/values-de/translations.xml | 30 ++++++ .../src/main/res/values-es/translations.xml | 4 + .../src/main/res/values-it/translations.xml | 4 + .../src/main/res/values-ro/translations.xml | 72 +++++++++++++ .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-de/translations.xml | 5 + .../src/main/res/values-de/translations.xml | 101 ++++++++++++++++++ .../src/main/res/values-es/translations.xml | 6 +- .../src/main/res/values-it/translations.xml | 6 +- .../src/main/res/values-ro/translations.xml | 25 ++++- .../src/main/res/values/localazy.xml | 51 +-------- 36 files changed, 419 insertions(+), 100 deletions(-) create mode 100644 features/createroom/impl/src/main/res/values-de/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-de/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-es/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-it/translations.xml create mode 100644 features/invitelist/impl/src/main/res/values-ro/translations.xml create mode 100644 features/login/impl/src/main/res/values-de/translations.xml create mode 100644 features/logout/api/src/main/res/values-de/translations.xml create mode 100644 features/onboarding/impl/src/main/res/values-de/translations.xml create mode 100644 features/rageshake/impl/src/main/res/values-de/translations.xml create mode 100644 features/roomdetails/impl/src/main/res/values-de/translations.xml create mode 100644 features/roomlist/impl/src/main/res/values-de/translations.xml create mode 100644 features/verifysession/impl/src/main/res/values-de/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-de/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-es/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-it/translations.xml create mode 100644 libraries/push/impl/src/main/res/values-ro/translations.xml create mode 100644 libraries/textcomposer/src/main/res/values-de/translations.xml diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..8a3c6cdeda --- /dev/null +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,7 @@ + + + "Neuer Raum" + "Privater Raum (nur auf Einladung)" + "Raumname" + "Thema (optional)" + \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index af6e3db1fa..a1ea3b31f0 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,6 +3,16 @@ "Cameră nouă" "Invitați persoane" "Adaugați persoane" + "A apărut o eroare la crearea camerei" + "Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior." + "Cameră privată (doar pe bază de invitație)" + "Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară." + "Cameră publică (oricine)" + "Numele camerei" + "e.g. Mici și Cozonaci" + "Creați o cameră" + "Subiect (opțional)" + "Despre ce este această cameră?" "A apărut o eroare la încercarea începerii conversației" "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..95e63cf5f2 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,6 @@ + + + "Chat ablehnen" + "Keine Einladungen" + "%1$s hat dich eingeladen" + \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-es/translations.xml b/features/invitelist/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..49c32a9e49 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s te invitó." + \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-it/translations.xml b/features/invitelist/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..c0eccfcb7c --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s ti ha invitato" + \ No newline at end of file diff --git a/features/invitelist/impl/src/main/res/values-ro/translations.xml b/features/invitelist/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..026485d102 --- /dev/null +++ b/features/invitelist/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,9 @@ + + + "Sigur doriți să refuzați alăturarea la %1$s?" + "Refuzați invitația" + "Sigur doriți să refuzați conversațiile cu %1$s?" + "Refuzați conversația" + "Nicio invitație" + "%1$s v-a invitat" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..061f3453df --- /dev/null +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,7 @@ + + + "Wie lautet die Adresse deines Servers?" + "Willkommen zurück!" + "Passwort" + "Benutzername" + \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index a299083994..284527c2f5 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -4,17 +4,17 @@ "Este servidor no soporta sliding sync." "Dirección del homeserver" "Solo puedes conectarte a un servidor que soporte sliding sync. El administrador de tu servidor tendrá que configurarlo. %1$s" - "Continuar" "¿Cuál es la dirección de tu servidor?" - "Selecciona tu servidor" "Esta cuenta ha sido desactivada." "Usuario y/o contraseña incorrectos" "Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'" "El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver." "Introduce tus datos" - "Contraseña" "Donde viven tus conversaciones" - "Continuar" "¡Hola de nuevo!" + "Continuar" + "Selecciona tu servidor" + "Contraseña" + "Continuar" "Usuario" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index 429f156883..b11875a18e 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -4,17 +4,17 @@ "Questo server attualmente non supporta la sincronizzazione scorrevole." "URL dell\'homeserver" "Puoi connetterti solo a un server esistente che supporta la sincronizzazione scorrevole. L\'amministratore del tuo server domestico dovrà configurarlo. %1$s" - "Continua" "Qual è l\'indirizzo del tuo server?" - "Seleziona il tuo server" "Questo profilo è stato disattivato." "Nome utente e/o password errati" "Questo non è un identificatore utente valido. Formato previsto: \'@user:homeserver.org\'" "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." "Inserisci i tuoi dati" - "Password" "Dove vivono le tue conversazioni" - "Continua" "Bentornato!" + "Continua" + "Seleziona il tuo server" + "Password" + "Continua" "Nome utente" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 2b5cce6829..349e3ddc04 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -4,17 +4,17 @@ "Momentan acest server nu oferă suport pentru sliding sync." "Adresa URL a homeserver-ului" "Vă putețo conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului dumneavoastră va trebui să îl configureze. %1$s" - "Continuați" "Care este adresa serverului dumneavoastră?" - "Selectați serverul" "Acest cont a fost dezactivat." "Utilizator și/sau parolă incorecte" "Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”" "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." "Introduceți detaliile" - "Parolă" "Locul unde trăiesc conversațiile tale" - "Continuați" "Bine ați revenit!" + "Continuați" + "Selectați serverul" + "Parola" + "Continuați" "Utilizator" \ No newline at end of file diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 82ea22e61b..6b0ecee43d 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -4,17 +4,17 @@ "This server currently doesn’t support sliding sync." "Homeserver URL" "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s" - "Continue" "What is the address of your server?" - "Select your server" "This account has been deactivated." "Incorrect username and/or password" "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’" "The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver." "Enter your details" - "Password" "Where your conversations live" - "Continue" "Welcome back!" + "Continue" + "Select your server" + "Password" + "Continue" "Username" \ No newline at end of file diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..9fd4f6b083 --- /dev/null +++ b/features/logout/api/src/main/res/values-de/translations.xml @@ -0,0 +1,7 @@ + + + "Abmelden" + "Abmelden" + "Abmeldung läuft…" + "Abmelden" + \ No newline at end of file diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..82e01aa522 --- /dev/null +++ b/features/onboarding/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Sei in deinem Element" + \ No newline at end of file diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..e18f43d1de --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,6 @@ + + + "Beschreibe den Fehler…" + "Absturzprotokolle senden" + "Bildschirmfoto senden" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..7581b585f1 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,15 @@ + + + + "1 Person" + "%1$d Personen" + + "Raum teilen" + "Blockieren" + "Nutzer blockieren" + "Blockierung aufheben" + "Nutzer entblockieren" + "Raum verlassen" + "Sicherheit" + "Thema" + \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index ba4327000b..58c486d6c3 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -4,18 +4,18 @@ "Una persona" "%1$d personas" + "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos." + "Cifrado de mensajes activado" + "Invitar a otras personas" + "Compartir sala" "Bloquear" - "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puedes revertir esta acción en cualquier momento." + "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento." "Bloquear usuario" "Desbloquear" "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." "Desbloquear usuario" - "Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos." - "Cifrado de mensajes activado" - "Invitar a otras personas" "Salir de la sala" "Personas" "Seguridad" - "Compartir sala" "Tema" \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index 9a980b79a9..a2e61a329c 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -4,18 +4,18 @@ "1 persona" "%1$d persone" + "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli." + "Crittografia messaggi abilitata" + "Invita persone" + "Condividi stanza" "Blocca" - "Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti i loro messaggi saranno nascosti. Potrai annullare questa azione in qualsiasi momento." + "Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento." "Blocca utente" "Sblocca" "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." "Sblocca utente" - "I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli." - "Crittografia messaggi abilitata" - "Invita persone" "Esci dalla stanza" "Persone" "Sicurezza" - "Condividi stanza" "Oggetto" \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index db6777fb7f..3525b06d8e 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -5,18 +5,18 @@ "%1$d persoane" + "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." + "Criptarea mesajelor este activată" + "Invitați persoane" + "Partajați camera" "Blocați" "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." "Blocați utilizatorul" "Deblocați" "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." "Deblocați utilizatorul" - "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." - "Criptarea mesajelor este activată" - "Invitați persoane" "Părăsiți camera" "Persoane" "Securitate" - "Partajați camera" "Subiect" \ No newline at end of file diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index f63757a8e3..584f4322d3 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -4,18 +4,18 @@ "1 person" "%1$d people" + "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." + "Message encryption enabled" + "Invite people" + "Share room" "Block" "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." "Block user" "Unblock" "On unblocking the user, you will be able to see all messages by them again." "Unblock user" - "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." - "Message encryption enabled" - "Invite people" "Leave room" "People" "Security" - "Share room" "Topic" \ No newline at end of file diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..00b1431f00 --- /dev/null +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,40 @@ + + + "Alle Chats" + "(Avatar wurde ebenfalls geändert)" + "%1$s hat seinen Avatar geändert" + "Du hast deinen Avatar geändert" + "%1$s hat den Anzeigenamen von %2$s in %3$s geändert" + "Du hast deinen Anzeigenamen von %1$s in %2$s geändert" + "%1$s hat den Anzeigenamen entfernt (war %2$s)" + "Du hast deinen Anzeigenamen entfernt (war %1$s)" + "%1$s hat den Anzeigenamen auf %2$s gesetzt" + "Du hast deinen Anzeigenamen auf %1$s gesetzt" + "%1$s hat den Raum-Avatar geändert" + "Du hast den Raum-Avatar geändert" + "%1$s hat den Raum-Avatar entfernt" + "%1$s hat den Raum erstellt" + "Du hast den Raum erstellt" + "%1$s hat %2$s eingeladen" + "%1$s hat die Einladung angenommen" + "Du hast die Einladung angenommen" + "Du hast %1$s eingeladen" + "%1$s hat dich eingeladen" + "%1$s ist dem Raum beigetreten" + "Du bist dem Raum beigetreten" + "%1$s hat deine Beitrittsanfrage abgelehnt" + "%1$s hat den Raum verlassen" + "Du hast den Raum verlassen" + "%1$s hat den Raumnamen geändert in: %2$s" + "Sie haben den Raumnamen geändert in: %1$s" + "%1$s hat den Raumnamen entfernt" + "Du hast den Raumnamen entfernt" + "%1$s hat die Einladung abgelehnt" + "Du hast die Einladung abgelehnt" + "%1$s hat %2$s entfernt" + "Du hast %1$s entfernt" + "%1$s hat das Thema geändert zu: %2$s" + "Sie haben das Thema geändert zu: %1$s" + "%1$s hat das Raumthema entfernt" + "Du hast das Raumthema entfernt" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..e3817c2507 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,10 @@ + + + "Emojis vergleichen" + "Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen." + "Ich bin bereit" + "Warten auf Übereinstimmung" + "Sie stimmen nicht überein" + "Sie stimmen überein" + "Verifizierung abgebrochen" + \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml index 839c945e24..ccc656e845 100644 --- a/features/verifysession/impl/src/main/res/values-es/translations.xml +++ b/features/verifysession/impl/src/main/res/values-es/translations.xml @@ -1,7 +1,6 @@ "Algo no fue bien. Se agotó el tiempo de espera de la solicitud o se rechazó." - "Verificación cancelada" "Confirma que los emojis que aparecen a continuación coinciden con los que aparecen en tu otra sesión." "Comparar emojis" "Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza." @@ -9,11 +8,12 @@ "Abrir una sesión existente" "Reintentar la verificación" "Estoy listo" - "Comenzar" "Esperando a que coincida" "Compara los emoji, asegurándote de que aparecen en el mismo orden." "No coinciden" "Coinciden" "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar." "A la espera de aceptar la solicitud" + "Verificación cancelada" + "Comenzar" \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 3d8a46d581..1bf0e87ea9 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -1,7 +1,6 @@ "C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata." - "Verifica annullata" "Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione." "Confronta le emoji" "La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile." @@ -9,11 +8,12 @@ "Apri una sessione esistente" "Riprova la verifica" "Sono pronto" - "Inizia" "In attesa di un riscontro" "Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine." "Non corrispondono" "Corrispondono" "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." "In attesa di accettare la richiesta" + "Verifica annullata" + "Inizia" \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index f2bade56fc..3ad0de6e56 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -1,7 +1,6 @@ "Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă." - "Verificare anulată" "Confirmați că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." "Comparați emoticoanele" "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere." @@ -9,11 +8,12 @@ "Deschideți o sesiune existentă" "Reîncercați verificarea" "Sunt pregătit" - "Începeți" "Se așteaptă confirmarea" "Comparăți emoticoalene asigurându-vă că apar în aceeași ordine." "Nu se potrivesc" "Se potrivesc" "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua." "Se așteptă acceptarea cererii" + "Verificare anulată" + "Începeți" \ No newline at end of file diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index fd81d104fb..c217f0d2a4 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -1,7 +1,6 @@ "Something doesn’t seem right. Either the request timed out or the request was denied." - "Verification cancelled" "Confirm that the emojis below match those shown on your other session." "Compare emojis" "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted." @@ -9,11 +8,12 @@ "Open an existing session" "Retry verification" "I am ready" - "Start" "Waiting to match" "Compare the unique emoji, ensuring they appear in the same order." "They don’t match" "They match" "Accept the request to start the verification process in your other session to continue." "Waiting to accept request" + "Verification cancelled" + "Start" \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..4d663c57aa --- /dev/null +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,30 @@ + + + "Laute Benachrichtigungen" + "Beitreten" + "Ablehnen" + "Neue Nachrichten" + "Als gelesen markieren" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s in %2$s und %3$s" + + "%1$s: %2$d Nachricht" + "%1$s: %2$d Nachrichten" + + + "%d Einladung" + "%d Einladungen" + + + "%d neue Nachricht" + "%d neue Nachrichten" + + + "%d Raum" + "%d Räume" + + "Google-Dienste" + "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." + "Schnellantwort" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-es/translations.xml b/libraries/push/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..31df508dc3 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "Respuesta rápida" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..32957fe2ce --- /dev/null +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Risposta rapida" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..9ed15ac738 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,72 @@ + + + "Apel" + "Ascultare evenimente" + "Notificări zgomotoase" + "Notificări silențioase" + "** Trimiterea eșuată - vă rugăm să deschideți camera" + "Alăturați-vă" + "Respingeți" + "Mesaje noi" + "Marcați ca citit" + "Eu" + "Vizualizați o notificare! Faceți clic pe mine!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s și %2$s" + "%1$s în %2$s" + "%1$s în %2$s și %3$s" + + + "%1$s: %2$d mesaj" + + + + "%1$s: %2$d mesaje" + + + + "%d notificare" + + + + "%d notificări" + + + + "%d invitație" + + + + "%d invitații" + + + + "%d mesaj nou" + + + + "%d mesaje noi" + + + + "%d mesaj notificat necitit" + + + + "%d mesaje notificate necitite" + + + + "%d cameră" + + + + "%d camere" + + "Alegeți modul de primire a notificărilor" + "Sincronizare în fundal" + "Servicii Google" + "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect." + "Raspuns rapid" + \ No newline at end of file diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 3a11adb5d3..d38bf7d8dd 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -9,7 +9,6 @@ "Reject" "New Messages" "Mark as read" - "Quick reply" "Me" "You are viewing the notification! Click me!" "%1$s: %2$s" @@ -45,4 +44,5 @@ "Background synchronization" "Google Services" "No valid Google Play Services found. Notifications may not work properly." + "Quick reply" \ No newline at end of file diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..f016d4bdba --- /dev/null +++ b/libraries/textcomposer/src/main/res/values-de/translations.xml @@ -0,0 +1,5 @@ + + + "Nachricht…" + "Link setzen" + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 22c60db481..4093aae962 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -1,5 +1,106 @@ + "Passwort ausblenden" + "Dateien senden" + "Passwort anzeigen" + "Benutzermenü" + "Zurück" + "Abbrechen" + "Foto auswählen" + "Schließen" + "Verifizierung abschließen" "Bestätigen" + "Kopieren" + "Link kopieren" + "Erstellen" + "Ablehnen" + "Deaktivieren" + "Fertig" + "Bearbeiten" + "Aktivieren" + "Einladen" + "Freunde zu %1$s einladen" + "Einladungen" + "Mehr erfahren" + "Verlassen" + "Raum verlassen" + "Weiter" + "Nein" + "OK" + "Schnellantwort" + "Zitieren" + "Entfernen" + "Fehler melden" + "Inhalt melden" + "Erneut versuchen" + "Entschlüsselung erneut versuchen" + "Speichern" + "Suchen" + "Senden" + "Nachricht senden" + "Teilen" + "Link teilen" + "Überspringen" + "Foto aufnehmen" + "Ja" + "Über" + "Analytik" + "Audio" + "Blasen" + "Entschlüsselungsfehler" + "Entwickleroptionen" + "(bearbeitet)" + "Verschlüsselung aktiviert" + "Fehler" + "Datei" + "GIF" + "Bild" + "Link in Zwischenablage kopiert" + "Nachricht" + "Modern" + "Offline" + "Passwort" + "Reaktionen" + "Sicherheit" + "Einstellungen" + "Sticker" + "Erfolg" + "Vorschläge" + "Thema" + "Entschlüsselung nicht möglich" + "Nicht unterstütztes Ereignis" + "Benutzername" + "Verifizierung abgebrochen" + "Verifizierung abgeschlossen" + "Video" + "Warten…" + "Warnung" + "Aktivitäten" + "Flaggen" + "Essen & Trinken" + "Tiere & Natur" + "Objekte" + "Smileys & Personen" + "Reisen & Orte" + "Symbole" + "Fehler beim Laden der Nachrichten" + "Entschuldigung, ein Fehler ist aufgetreten." + "%1$s Android" + + "%1$d Mitglied" + "%1$d Mitglieder" + + "Grund für die Meldung dieses Inhalts" + "Dies ist der Anfang von %1$s." + "Neu" + "Blockieren" + "Nutzer blockieren" + "Blockierung aufheben" + "Nutzer entblockieren" + "Erkennungsschwelle" + "Version: %1$s (%2$s)" "de" + "Fehler" + "Erfolg" + "Nutzer blockieren" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 564ede34a8..b430048f79 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -94,8 +94,6 @@ "Vídeo" "Esperando…" "Confirmar" - "Error" - "Terminado" "Atención" "Actividades" "Banderas" @@ -129,7 +127,6 @@ "Este es el principio de %1$s." "Este es el principio de esta conversación." "Nuevos" - "Bloquear usuario" "Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario" "Bloquear" "Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento." @@ -142,4 +139,7 @@ "General" "Versión: %1$s (%2$s)" "es" + "Error" + "Terminado" + "Bloquear usuario" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index a8ec05115a..8fbe54dae2 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -94,8 +94,6 @@ "Video" "In attesa…" "Conferma" - "Errore" - "Operazione riuscita" "Attenzione" "Attività" "Bandiere" @@ -129,7 +127,6 @@ "Questo è l\'inizio di %1$s." "Questo è l\'inizio della conversazione." "Nuovo" - "Blocca utente" "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" "Blocca" "Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento." @@ -142,4 +139,7 @@ "Generali" "Versione: %1$s (%2$s)" "it" + "Errore" + "Operazione riuscita" + "Blocca utente" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 091dd7f36d..2a9dc93490 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -4,8 +4,10 @@ "Trimiteți fișiere" "Afișați parola" "Meniu utilizator" + "Acceptați" "Înapoi" "Anulați" + "Alegeți o fotografie" "Ștergeți" "Închideți" "Verificare completă" @@ -13,13 +15,16 @@ "Continuați" "Copiați" "Copiați linkul" + "Creați" "Creați o cameră" + "Refuzați" "Dezactivați" "Efectuat" "Editați" "Activați" "Invitați" "Invitați prieteni în %1$s" + "Invitații" "Aflați mai multe" "Părăsiți" "Părăsiți camera" @@ -38,15 +43,18 @@ "Salvați" "Căutați" "Trimiteți" + "Trimiteți mesajul" "Partajați" "Partajați linkul" "Omiteți" "Începeți" "Începeți discuția" "Începeți verificarea" + "Faceți o fotografie" "Vedeți sursă" "Da" "Despre" + "Analitice" "Audio" "Baloane" "Se creează camera…" @@ -94,8 +102,6 @@ "Video" "Se aşteaptă…" "Confirmare" - "Eroare" - "Succes" "Avertisment" "Activități" "Steaguri" @@ -131,7 +137,17 @@ "Acesta este începutul conversației %1$s." "Acesta este începutul acestei conversații." "Nou" - "Blocați utilizatorul" + "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." + "Nu"" înregistrăm sau profilăm datele contului" + "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." + "Puteți citi toate condițiile noastre %1$s." + "aici" + "Puteți dezactiva această opțiune oricând din setări" + "Nu"" împărtășim informații cu terți" + "Ajutați la îmbunătățirea %1$s" + "Puteți citi toate condițiile noastre %1$s." + "aici" + "Partajați datele analitice" "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" "Blocați" "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." @@ -144,4 +160,7 @@ "General" "Versiunea: %1$s (%2$s)" "ro" + "Eroare" + "Succes" + "Blocați utilizatorul" \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index bfd2be2000..5df318b7ae 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -102,8 +102,6 @@ "Video" "Waiting…" "Confirmation" - "Error" - "Success" "Warning" "Activities" "Flags" @@ -122,60 +120,15 @@ "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" - "Call" - "Listening for events" - "Noisy notifications" - "Silent notifications" - "** Failed to send - please open room" - "Join" - "Reject" - "New Messages" - "Mark as read" - "Quick reply" - "Me" - "You are viewing the notification! Click me!" - "%1$s: %2$s" - "%1$s: %2$s %3$s" - "%1$s and %2$s" - "%1$s in %2$s" - "%1$s in %2$s and %3$s" "%1$d member" "%1$d members" - - "%1$s: %2$d message" - "%1$s: %2$d messages" - - - "%d notification" - "%d notifications" - - - "%d invitation" - "%d invitations" - - - "%d new message" - "%d new messages" - - - "%d unread notified message" - "%d unread notified messages" - - - "%d room" - "%d rooms" - "%1$d room change" "%1$d room changes" "Rageshake to report bug" - "Choose how to receive notifications" - "Background synchronization" - "Google Services" - "No valid Google Play Services found. Notifications may not work properly." "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" @@ -193,7 +146,6 @@ "You can read all our terms %1$s." "here" "Share analytics data" - "Block user" "Check if you want to hide all current and future messages from this user" "Block" "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime." @@ -207,4 +159,7 @@ "Version: %1$s (%2$s)" "en" "en" + "Error" + "Success" + "Block user" \ No newline at end of file From 77656782d6973bab471d01e22384661d81a52d48 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 10:28:07 +0200 Subject: [PATCH 05/33] trigger ci From e65adaecd15c0a34786e4cf0b0a69c22a7b90e69 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 24 Apr 2023 10:42:27 +0200 Subject: [PATCH 06/33] Timeline: add ContentType instead of using introspection. --- .../features/messages/impl/timeline/TimelineView.kt | 11 ----------- .../messages/impl/timeline/diff/CacheInvalidator.kt | 8 ++++---- .../messages/impl/timeline/model/TimelineItem.kt | 5 +++++ .../timeline/model/event/TimelineItemEmoteContent.kt | 4 +++- .../model/event/TimelineItemEncryptedContent.kt | 4 +++- .../timeline/model/event/TimelineItemEventContent.kt | 4 +++- .../timeline/model/event/TimelineItemImageContent.kt | 4 +++- .../timeline/model/event/TimelineItemNoticeContent.kt | 4 +++- .../model/event/TimelineItemRedactedContent.kt | 4 +++- .../timeline/model/event/TimelineItemTextContent.kt | 4 +++- .../model/event/TimelineItemUnknownContent.kt | 4 +++- .../model/virtual/TimelineItemDaySeparatorModel.kt | 4 +++- .../model/virtual/TimelineItemLoadingModel.kt | 4 +++- .../model/virtual/TimelineItemReadMarkerModel.kt | 4 +++- .../model/virtual/TimelineItemTimelineStartModel.kt | 4 +++- .../model/virtual/TimelineItemUnknownVirtualModel.kt | 4 +++- .../model/virtual/TimelineItemVirtualModel.kt | 4 +++- 17 files changed, 51 insertions(+), 29 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index c7b071e069..a378c508f6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -75,7 +75,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import timber.log.Timber @Composable fun TimelineView( @@ -115,24 +114,14 @@ fun TimelineView( } } - /* TimelineScrollHelper( lazyListState = lazyListState, timelineItems = state.timelineItems, onLoadMore = ::onReachedLoadMore ) - - */ } } -private fun TimelineItem.contentType() = when (this) { - is TimelineItem.Event -> content.javaClass.simpleName - is TimelineItem.Virtual -> model.javaClass.simpleName -}.also { - Timber.v("ContentType = $it") -} - @Composable fun TimelineItemRow( timelineItem: TimelineItem, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt index c2f3b33667..9aa3ab5e02 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt @@ -25,7 +25,7 @@ internal class CacheInvalidator(private val itemStatesCache: MutableList id } + fun contentType(): String = when (this) { + is Event -> content.type + is Virtual -> model.type + } + @Immutable data class Virtual( val id: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt index 67fefb665e..a059c9b275 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEmoteContent.kt @@ -21,4 +21,6 @@ import org.jsoup.nodes.Document data class TimelineItemEmoteContent( override val body: String, override val htmlDocument: Document? -) : TimelineItemTextBasedContent +) : TimelineItemTextBasedContent { + override val type: String = "TimelineItemEmoteContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt index b42c2ac535..ff1bb36faf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContent.kt @@ -20,4 +20,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry data class TimelineItemEncryptedContent( val data: UnableToDecryptContent.Data -) : TimelineItemEventContent +) : TimelineItemEventContent { + override val type: String = "TimelineItemEncryptedContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index 7e166dba97..233f51a5a2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -19,4 +19,6 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.runtime.Immutable @Immutable -sealed interface TimelineItemEventContent +sealed interface TimelineItemEventContent { + val type: String +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 8013ca8a65..1d2367d13f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -23,4 +23,6 @@ data class TimelineItemImageContent( val imageMeta: MediaResolver.Meta, val blurhash: String?, val aspectRatio: Float -) : TimelineItemEventContent +) : TimelineItemEventContent{ + override val type: String = "TimelineItemImageContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt index 6c647158c4..7974f188a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemNoticeContent.kt @@ -21,4 +21,6 @@ import org.jsoup.nodes.Document data class TimelineItemNoticeContent( override val body: String, override val htmlDocument: Document? -) : TimelineItemTextBasedContent +) : TimelineItemTextBasedContent { + override val type: String = "TimelineItemNoticeContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt index de98b22bbb..7a8edae953 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRedactedContent.kt @@ -16,4 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.event -object TimelineItemRedactedContent : TimelineItemEventContent +object TimelineItemRedactedContent : TimelineItemEventContent{ + override val type: String = "TimelineItemRedactedContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt index 6cbb0ccd08..993275e627 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemTextContent.kt @@ -21,4 +21,6 @@ import org.jsoup.nodes.Document data class TimelineItemTextContent( override val body: String, override val htmlDocument: Document? -) : TimelineItemTextBasedContent +) : TimelineItemTextBasedContent{ + override val type: String = "TimelineItemTextContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt index eb79b29f79..44aeb93e3b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemUnknownContent.kt @@ -16,4 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.event -object TimelineItemUnknownContent : TimelineItemEventContent +object TimelineItemUnknownContent : TimelineItemEventContent { + override val type: String = "TimelineItemUnknownContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt index b4c1235f8f..54e95b7294 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModel.kt @@ -18,4 +18,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual data class TimelineItemDaySeparatorModel( val formattedDate: String -) : TimelineItemVirtualModel +) : TimelineItemVirtualModel { + override val type: String = "TimelineItemDaySeparatorModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt index 4870177a84..9cc7280b07 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemLoadingModel.kt @@ -16,4 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -object TimelineItemLoadingModel : TimelineItemVirtualModel +object TimelineItemLoadingModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemLoadingModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt index 247cd58212..0b8e3fc0e5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt @@ -16,4 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -object TimelineItemReadMarkerModel : TimelineItemVirtualModel +object TimelineItemReadMarkerModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemReadMarkerModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt index ab0ec4fdf8..8c1afea886 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemTimelineStartModel.kt @@ -16,4 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -object TimelineItemTimelineStartModel : TimelineItemVirtualModel +object TimelineItemTimelineStartModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemTimelineStartModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt index 6d023bf748..8b4fe44744 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemUnknownVirtualModel.kt @@ -16,4 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -object TimelineItemUnknownVirtualModel : TimelineItemVirtualModel +object TimelineItemUnknownVirtualModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemUnknownVirtualModel" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt index a7911867f9..d6c3529ab4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemVirtualModel.kt @@ -19,4 +19,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual import androidx.compose.runtime.Immutable @Immutable -sealed interface TimelineItemVirtualModel +sealed interface TimelineItemVirtualModel { + val type: String +} From dfabc02bf6e4359f8be33e6b32555ae20f81ac1a Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 24 Apr 2023 10:43:37 +0200 Subject: [PATCH 07/33] Timeline : Add isInit to avoid calling rust methods when the timeline is not ready. --- .../libraries/matrix/impl/timeline/RustMatrixTimeline.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 6d19e817a4..3222cda704 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -42,6 +42,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineListener import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean class RustMatrixTimeline( private val matrixRoom: MatrixRoom, @@ -51,6 +52,8 @@ class RustMatrixTimeline( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixTimeline { + private val isInit = AtomicBoolean(false) + private val timelineItems: MutableStateFlow> = MutableStateFlow(emptyList()) @@ -95,6 +98,7 @@ class RustMatrixTimeline( withContext(coroutineDispatchers.diffUpdateDispatcher) { this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems } + isInit.set(true) } .onFailure { Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})") @@ -105,6 +109,7 @@ class RustMatrixTimeline( override fun dispose() { Timber.v("Dispose timeline for room ${matrixRoom.roomId}") listenerTokens.dispose() + isInit.set(false) } /** @@ -125,6 +130,9 @@ class RustMatrixTimeline( override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result = withContext(coroutineDispatchers.io) { runCatching { Timber.v("Start back paginating for room ${matrixRoom.roomId} ") + if (!isInit.get()) { + throw IllegalStateException("Timeline is not init yet") + } val paginationOptions = PaginationOptions.UntilNumItems( eventLimit = requestSize.toUShort(), items = untilNumberOfItems.toUShort() From f601bb5c1b75136dae5654c8bd4afab7b27bdcad Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 24 Apr 2023 10:57:05 +0200 Subject: [PATCH 08/33] Timeline: copy getInitial method from EA to avoid showing @ as avatar. --- .../designsystem/components/avatar/Avatar.kt | 2 +- .../components/avatar/AvatarData.kt | 34 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 06aa4435a0..05257ece7f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -93,7 +93,7 @@ private fun InitialsAvatar( ) { Text( modifier = Modifier.align(Alignment.Center), - text = avatarData.getInitial(), + text = avatarData.initial, fontSize = (avatarData.size.dp / 2).value.sp, color = Color.White, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index 3bf4f7d0b4..e46751f15f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -30,8 +30,36 @@ data class AvatarData( @IgnoredOnParcel val size: AvatarSize = AvatarSize.MEDIUM ) : Parcelable { - fun getInitial(): String { - val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?' - return firstChar.uppercase() + + val initial by lazy { + (name?.takeIf { it.isNotBlank() } ?: id) + .let { dn -> + var startIndex = 0 + val initial = dn[startIndex] + + if (initial in listOf('@', '#', '+') && dn.length > 1) { + startIndex++ + } + + var length = 1 + var first = dn[startIndex] + + // LEFT-TO-RIGHT MARK + if (dn.length >= 2 && 0x200e == first.code) { + startIndex++ + first = dn[startIndex] + } + + // check if it’s the start of a surrogate pair + if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) { + val second = dn[startIndex + 1] + if (second.code in 0xDC00..0xDFFF) { + length++ + } + } + + dn.substring(startIndex, startIndex + length) + } + .uppercase() } } From 913e6465cb12c705d67f417ebf1b2ac3bcb4e87e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 17:04:20 +0200 Subject: [PATCH 09/33] Remove extra plural items manually until the issue on Localazy is fixed. --- .../src/main/res/values-ro/translations.xml | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml index 9ed15ac738..47280228a4 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -17,51 +17,33 @@ "%1$s în %2$s" "%1$s în %2$s și %3$s" - "%1$s: %2$d mesaj" - - "%1$s: %2$d mesaje" - "%d notificare" - - "%d notificări" - "%d invitație" - - "%d invitații" - "%d mesaj nou" - - "%d mesaje noi" - "%d mesaj notificat necitit" - - "%d mesaje notificate necitite" - "%d cameră" - - "%d camere" "Alegeți modul de primire a notificărilor" @@ -69,4 +51,4 @@ "Servicii Google" "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect." "Raspuns rapid" - \ No newline at end of file + From 1ab96e2f34753e7c89280025026e846eff004325 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Apr 2023 11:17:05 +0200 Subject: [PATCH 10/33] Some clean up --- .../main/kotlin/io/element/android/x/ElementXApplication.kt | 2 +- .../libraries/designsystem/components/avatar/AvatarData.kt | 1 + .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 4 ++-- .../element/android/libraries/matrix/ui/media/MediaKeyer.kt | 5 ++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index 2b02bf3700..31dab9ad48 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -19,8 +19,8 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer import io.element.android.x.di.AppComponent -import io.element.android.x.di.DaggerAppComponent import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.x.di.DaggerAppComponent import io.element.android.x.info.logApplicationInfo import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.MatrixInitializer diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index e46751f15f..2d1e0558f4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -31,6 +31,7 @@ data class AvatarData( val size: AvatarSize = AvatarSize.MEDIUM ) : Parcelable { + @IgnoredOnParcel val initial by lazy { (name?.takeIf { it.isNotBlank() } ?: id) .let { dn -> diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 11558e86b6..4efe35f6b5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -184,13 +184,13 @@ class RustMatrixRoom( } override suspend fun acceptInvitation(): Result = withContext(coroutineDispatchers.io) { - kotlin.runCatching { + runCatching { innerRoom.acceptInvitation() } } override suspend fun rejectInvitation(): Result = withContext(coroutineDispatchers.io) { - kotlin.runCatching { + runCatching { innerRoom.rejectInvitation() } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt index 2d4ab683b1..6a1a5e8bfb 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt @@ -33,4 +33,7 @@ internal class MediaKeyer : Keyer { } } -private fun MediaResolver.Meta.toKey() = "${url}_${kind}" +private fun MediaResolver.Meta.toKey(): String? { + if (url.isNullOrBlank()) return null + return "${url}_${kind}" +} From 10ea382e3506bbf0d820599b8f1b92fc873dacc3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Apr 2023 11:37:27 +0200 Subject: [PATCH 11/33] Generate new snapshots --- ...Group_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...dalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...alBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 78ce1fa217..5555d6e6b3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9 -size 4478 +oid sha256:21698ff1c1f2b30ee9c3cc0c2539b35fe7cf54aac07cb0dc376d7c1a03c8814b +size 4483 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 78ce1fa217..5555d6e6b3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9 -size 4478 +oid sha256:21698ff1c1f2b30ee9c3cc0c2539b35fe7cf54aac07cb0dc376d7c1a03c8814b +size 4483 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 88a70ce698..c18cc0dd08 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49aafbdb693cccd3ab7ca94e61f7a1de437766a29967dd89c4aaf5134b55c004 -size 14860 +oid sha256:db69f27f60dd9d93bb4d313741b84aa4a3ed008d229590338514c7683c0e3a11 +size 14786 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png index 665c8811ac..eb47d1cd71 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b -size 4457 +oid sha256:fc3bf884b0425c72cafecdd4afa4e2c28064799f695962360ae4c979a3fe542e +size 4490 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png index 665c8811ac..eb47d1cd71 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b -size 4457 +oid sha256:fc3bf884b0425c72cafecdd4afa4e2c28064799f695962360ae4c979a3fe542e +size 4490 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png index f7378511e1..94f6bbbd99 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:857fedd347931600aa5613565887384579b565efb86d5eeca6e93c13f5ff442c -size 13994 +oid sha256:6c630475e03d86195a0ebcc57bd12934b799fee956c635b30df60913cd9a3f50 +size 16032 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png index a4aa94b6e4..a64e1b5a7a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddbb6611ae83055106f7b67ec828542f8a896cafb49001ed0baef43633cc77c1 -size 8884 +oid sha256:428372bd789ff5e83ba4d837bbb0592edd8a01571728c7b284c57ddb200226ed +size 9407 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png index 317643c598..9aa01dc971 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ModalBottomSheetLayoutLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c9a3c9f68a6627654856b03d2534ae1e4e8e600989bc3719407fbf8e17a7ab1 -size 8631 +oid sha256:718cf6e3323ceb9bbf8b4dd9752203d5137840bd3dfb538008d579511b177412 +size 9504 From 0234553bca0d10ce0a6e8a3fc27f5901997ddd5c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 25 Apr 2023 13:35:36 +0200 Subject: [PATCH 12/33] [Room list] Search & menu improvements (#356) * Remove settings menu item, start splitting search UI. Also, add `applyIf` and `circularReveal` modifiers. * Split UI & logic for room list search * Suppress `composed` warning, improve its debuggability * Add content description to the user's avatar, fix window insets. Also, remove unused `SearchRoomListTopBar`. --- changelog.d/354.feature | 1 + .../networkmonitor/impl/NetworkMonitorImpl.kt | 4 +- .../features/roomlist/impl/RoomListEvents.kt | 1 + .../roomlist/impl/RoomListPresenter.kt | 33 ++- .../features/roomlist/impl/RoomListState.kt | 4 +- .../roomlist/impl/RoomListStateProvider.kt | 4 + .../features/roomlist/impl/RoomListView.kt | 60 ++++-- .../impl/components/RoomListTopBar.kt | 151 ++----------- .../roomlist/impl/search/RoomListSearch.kt | 201 ++++++++++++++++++ .../roomlist/impl/RoomListPresenterTests.kt | 9 +- .../designsystem/components/avatar/Avatar.kt | 12 +- .../designsystem/modifiers/ApplyIf.kt | 45 ++++ .../designsystem/modifiers/CircularReveal.kt | 106 +++++++++ .../utils/WindowInsetsExtension.kt | 39 ++++ ...pBarDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...BarLightPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...tContentPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + 31 files changed, 534 insertions(+), 199 deletions(-) create mode 100644 changelog.d/354.feature create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png diff --git a/changelog.d/354.feature b/changelog.d/354.feature new file mode 100644 index 0000000000..7d6e15b545 --- /dev/null +++ b/changelog.d/354.feature @@ -0,0 +1 @@ +Improve room list search and general UI diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt index ba4d6c2775..25819c6eb3 100644 --- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt +++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/NetworkMonitorImpl.kt @@ -71,8 +71,8 @@ class NetworkMonitorImpl @Inject constructor( private fun listenToConnectionChanges() { val request = NetworkRequest.Builder() - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) +// .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) +// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback(request, callback) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index 299c670eb4..684342bee8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -20,4 +20,5 @@ sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents object DismissRequestVerificationPrompt : RoomListEvents + object ToggleSearchResults : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 38c80734e2..f366c25326 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -79,6 +79,7 @@ class RoomListPresenter @Inject constructor( Timber.v("RoomSummaries size = ${roomSummaries.size}") + val mappedRoomSummaries: MutableState> = remember { mutableStateOf(persistentListOf()) } val filteredRoomSummaries: MutableState> = remember { mutableStateOf(persistentListOf()) } @@ -101,41 +102,51 @@ class RoomListPresenter @Inject constructor( derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed } } + var displaySearchResults by rememberSaveable { mutableStateOf(false) } + fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateFilter -> filter = event.newFilter is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true + RoomListEvents.ToggleSearchResults -> { + if (displaySearchResults) { + filter = "" + } + displaySearchResults =! displaySearchResults + } } } LaunchedEffect(roomSummaries, filter) { - filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter) + mappedRoomSummaries.value = if (roomSummaries.isEmpty()) { + RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList() + } else { + mapRoomSummaries(roomSummaries).toImmutableList() + } + filteredRoomSummaries.value = updateFilteredRoomSummaries(mappedRoomSummaries.value, filter) } val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) return RoomListState( matrixUser = matrixUser.value, - roomList = filteredRoomSummaries.value, + roomList = mappedRoomSummaries.value, filter = filter, + filteredRoomList = filteredRoomSummaries.value, displayVerificationPrompt = displayVerificationPrompt, snackbarMessage = snackbarMessage, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, displayInvites = invites.isNotEmpty(), + displaySearchResults = displaySearchResults, eventSink = ::handleEvents ) } - private suspend fun updateFilteredRoomSummaries(roomSummaries: List?, filter: String): ImmutableList { - if (roomSummaries.isNullOrEmpty()) { - return RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList() - } - val mappedRoomSummaries = mapRoomSummaries(roomSummaries) - return if (filter.isEmpty()) { - mappedRoomSummaries - } else { - mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) } + private fun updateFilteredRoomSummaries(mappedRoomSummaries: ImmutableList, filter: String): ImmutableList { + return when { + filter.isEmpty() -> emptyList() + else -> mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) } }.toImmutableList() } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 185c7c94a9..6fd629d67f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -26,10 +26,12 @@ import kotlinx.collections.immutable.ImmutableList data class RoomListState( val matrixUser: MatrixUser?, val roomList: ImmutableList, - val filter: String, + val filter: String?, + val filteredRoomList: ImmutableList, val displayVerificationPrompt: Boolean, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val displayInvites: Boolean, + val displaySearchResults: Boolean, val eventSink: (RoomListEvents) -> Unit ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 1dfa943660..a95456b6a4 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -36,6 +36,8 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)), aRoomListState().copy(hasNetworkConnection = false), aRoomListState().copy(displayInvites = true), + aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), + aRoomListState().copy(displaySearchResults = true), ) } @@ -43,10 +45,12 @@ internal fun aRoomListState() = RoomListState( matrixUser = MatrixUser(id = UserId("@id:domain"), username = "User#1", avatarData = AvatarData("@id:domain", "U")), roomList = aRoomListRoomSummaryList(), filter = "filter", + filteredRoomList = aRoomListRoomSummaryList(), hasNetworkConnection = true, snackbarMessage = null, displayVerificationPrompt = false, displayInvites = false, + displaySearchResults = false, eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 8c591f2c2b..cd42548007 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -64,6 +64,8 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchResultContent +import io.element.android.features.roomlist.impl.search.RoomListSearchResultView import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -91,15 +93,27 @@ fun RoomListView( onCreateRoomClicked: () -> Unit = {}, onInvitesClicked: () -> Unit = {}, ) { - RoomListContent( - state = state, - modifier = modifier, - onRoomClicked = onRoomClicked, - onOpenSettings = onOpenSettings, - onVerifyClicked = onVerifyClicked, - onCreateRoomClicked = onCreateRoomClicked, - onInvitesClicked = onInvitesClicked, - ) + Column(modifier = modifier) { + ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) + Box { + RoomListContent( + state = state, + onRoomClicked = onRoomClicked, + onOpenSettings = onOpenSettings, + onVerifyClicked = onVerifyClicked, + onCreateRoomClicked = onCreateRoomClicked, + onInvitesClicked = onInvitesClicked, + ) + // This overlaid view will only be visible when state.displaySearchResults is true + RoomListSearchResultView( + state = state, + onRoomClicked = onRoomClicked, + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) + } + } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -163,16 +177,14 @@ fun RoomListContent( Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - Column { - ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) - RoomListTopBar( - matrixUser = state.matrixUser, - filter = state.filter, - onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, - onOpenSettings = onOpenSettings, - scrollBehavior = scrollBehavior, - ) - } + RoomListTopBar( + matrixUser = state.matrixUser, + areSearchResultsDisplayed = state.displaySearchResults, + onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, + onOpenSettings = onOpenSettings, + scrollBehavior = scrollBehavior, + ) }, content = { padding -> Column( @@ -306,7 +318,7 @@ internal fun PreviewRequestVerificationHeaderDark() { } } -private fun RoomListRoomSummary.contentType() = isPlaceholder +internal fun RoomListRoomSummary.contentType() = isPlaceholder @Preview @Composable @@ -322,3 +334,11 @@ internal fun RoomListViewDarkPreview(@PreviewParameter(RoomListStateProvider::cl private fun ContentToPreview(state: RoomListState) { RoomListView(state) } + +@Preview +@Composable +internal fun RoomListSearchResultContentPreview() { + ElementPreviewLight { + RoomListSearchResultContent(state = aRoomListState(), onRoomClicked = {}) + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 1d1a4e7c10..48c8ac0253 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -20,57 +20,40 @@ package io.element.android.features.roomlist.impl.components import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import io.element.android.features.roomlist.impl.R import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextField -import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListTopBar( matrixUser: MatrixUser?, - filter: String, + areSearchResultsDisplayed: Boolean, onFilterChanged: (String) -> Unit, + onToggleSearch: () -> Unit, onOpenSettings: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier, @@ -79,124 +62,26 @@ fun RoomListTopBar( tag = "RoomListScreen", msg = "TopBar" ) - var searchWidgetStateIsOpened by rememberSaveable { mutableStateOf(false) } fun closeFilter() { onFilterChanged("") - searchWidgetStateIsOpened = false } - BackHandler(enabled = searchWidgetStateIsOpened) { + BackHandler(enabled = areSearchResultsDisplayed) { closeFilter() + onToggleSearch() } - if (searchWidgetStateIsOpened) { - SearchRoomListTopBar( - text = filter, - onFilterChanged = onFilterChanged, - onCloseClicked = ::closeFilter, - scrollBehavior = scrollBehavior, - modifier = modifier, - ) - } else { - DefaultRoomListTopBar( - matrixUser = matrixUser, - onOpenSettings = onOpenSettings, - onSearchClicked = { - searchWidgetStateIsOpened = true - }, - scrollBehavior = scrollBehavior, - modifier = modifier, - ) - } -} - -@Composable -fun SearchRoomListTopBar( - text: String, - scrollBehavior: TopAppBarScrollBehavior, - modifier: Modifier = Modifier, - onFilterChanged: (String) -> Unit = {}, - onCloseClicked: () -> Unit = {}, -) { - var filterState by textFieldState(stateValue = text) - val focusRequester = remember { FocusRequester() } - TopAppBar( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - title = { - TextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = filterState, - textStyle = TextStyle( - fontSize = 17.sp - ), - onValueChange = { - filterState = it - onFilterChanged(it) - }, - placeholder = { - Text( - text = stringResource(id = StringR.string.action_search), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium) - ) - }, - singleLine = true, - trailingIcon = { - if (text.isNotEmpty()) { - IconButton( - onClick = { - onFilterChanged("") - } - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "clear", - tint = MaterialTheme.colorScheme.onBackground - ) - } - } - }, - ) - }, - navigationIcon = { - IconButton( - onClick = { - onCloseClicked() - } - ) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "close", - tint = MaterialTheme.colorScheme.onBackground - ) - } - }, - windowInsets = WindowInsets(0.dp) - ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } -} - -@Preview -@Composable -internal fun SearchRoomListTopBarLightPreview() = ElementPreviewLight { SearchRoomListTopBarPreview() } - -@Preview -@Composable -internal fun SearchRoomListTopBarDarkPreview() = ElementPreviewDark { SearchRoomListTopBarPreview() } - -@Composable -private fun SearchRoomListTopBarPreview() { - SearchRoomListTopBar( - text = "Hello", - scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), + DefaultRoomListTopBar( + matrixUser = matrixUser, + onOpenSettings = onOpenSettings, + onSearchClicked = onToggleSearch, + scrollBehavior = scrollBehavior, + modifier = modifier, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBar( matrixUser: MatrixUser?, @@ -216,8 +101,8 @@ private fun DefaultRoomListTopBar( }, navigationIcon = { if (matrixUser != null) { - IconButton(onClick = {}) { - Avatar(matrixUser.avatarData) + IconButton(onClick = onOpenSettings) { + Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings)) } } }, @@ -225,12 +110,7 @@ private fun DefaultRoomListTopBar( IconButton( onClick = onSearchClicked ) { - Icon(Icons.Default.Search, contentDescription = "search") - } - IconButton( - onClick = onOpenSettings - ) { - Icon(Icons.Default.Settings, contentDescription = "Settings") + Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search)) } }, scrollBehavior = scrollBehavior, @@ -246,6 +126,7 @@ internal fun DefaultRoomListTopBarLightPreview() = ElementPreviewLight { Default @Composable internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRoomListTopBarPreview() } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DefaultRoomListTopBarPreview() { DefaultRoomListTopBar( diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt new file mode 100644 index 0000000000..c2baca9a6b --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearch.kt @@ -0,0 +1,201 @@ +/* + * 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.features.roomlist.impl.search + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import io.element.android.features.roomlist.impl.RoomListEvents +import io.element.android.features.roomlist.impl.RoomListState +import io.element.android.features.roomlist.impl.components.RoomSummaryRow +import io.element.android.features.roomlist.impl.contentType +import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.modifiers.applyIf +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.copy +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.R + +@Composable +internal fun RoomListSearchResultView( + state: RoomListState, + onRoomClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = state.displaySearchResults, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column( + modifier = modifier + .applyIf(state.displaySearchResults, ifTrue = { + // Disable input interaction to underlying views + pointerInput(Unit) {} + }) + ) { + if (state.displaySearchResults) { + RoomListSearchResultContent(state = state, onRoomClicked = onRoomClicked) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun RoomListSearchResultContent( + state: RoomListState, + onRoomClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + val borderColor = MaterialTheme.colorScheme.tertiary + val strokeWidth = 1.dp + fun onBackButtonPressed() { + state.eventSink(RoomListEvents.ToggleSearchResults) + } + fun onRoomClicked(room: RoomListRoomSummary) { + if (room.roomId == null) return + onRoomClicked(room.roomId) + } + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + modifier = Modifier.drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = strokeWidth.value + ) + }, + navigationIcon = { BackButton(onClick = ::onBackButtonPressed) }, + title = { + val filter = state.filter.orEmpty() + val focusRequester = FocusRequester() + TextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = filter, + onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + trailingIcon = { + if (filter.isNotEmpty()) { + IconButton(onClick = { + state.eventSink(RoomListEvents.UpdateFilter("")) + }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.action_cancel) + ) + } + } + } + ) + + LaunchedEffect(state.displaySearchResults) { + if (state.displaySearchResults) { + focusRequester.requestFocus() + } + } + }, + windowInsets = TopAppBarDefaults.windowInsets.copy(top = 0) + ) + } + ) { padding -> + val lazyListState = rememberLazyListState() + val visibleRange by remember { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + val firstItemIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0 + val size = layoutInfo.visibleItemsInfo.size + firstItemIndex until firstItemIndex + size + } + } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity + ): Velocity { + state.eventSink(RoomListEvents.UpdateVisibleRange(visibleRange)) + return super.onPostFling(consumed, available) + } + } + } + Column( + modifier = Modifier + .padding(padding) + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .nestedScroll(nestedScrollConnection), + state = lazyListState, + ) { + items( + items = state.filteredRoomList, + contentType = { room -> room.contentType() }, + ) { room -> + RoomSummaryRow(room = room, onClick = ::onRoomClicked) + } + } + } + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index e28eb1e362..5dd73980f8 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -112,6 +112,8 @@ class RoomListPresenterTests { withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) val withFilterState = awaitItem() Truth.assertThat(withFilterState.filter).isEqualTo("t") + + cancelAndIgnoreRemainingEvents() } } @@ -168,17 +170,18 @@ class RoomListPresenterTests { val loadedState = awaitItem() // Test filtering with result loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3))) + skipItems(1) // Filter update val withNotFilteredRoomState = awaitItem() Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3)) - Truth.assertThat(withNotFilteredRoomState.roomList.size).isEqualTo(1) - Truth.assertThat(withNotFilteredRoomState.roomList.first()) + Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1) + Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first()) .isEqualTo(aRoomListRoomSummary) // Test filtering without result withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada")) skipItems(1) // Filter update val withFilteredRoomState = awaitItem() Truth.assertThat(withFilteredRoomState.filter).isEqualTo("tada") - Truth.assertThat(withFilteredRoomState.roomList).isEmpty() + Truth.assertThat(withFilteredRoomState.filteredRoomList).isEmpty() } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 06aa4435a0..02758901f1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -41,7 +41,11 @@ import io.element.android.libraries.designsystem.theme.components.Text import timber.log.Timber @Composable -fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { +fun Avatar( + avatarData: AvatarData, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { val commonModifier = modifier .size(avatarData.size.dp) .clip(CircleShape) @@ -54,6 +58,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { ImageAvatar( avatarData = avatarData, modifier = commonModifier, + contentDescription = contentDescription, ) } } @@ -62,13 +67,14 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { private fun ImageAvatar( avatarData: AvatarData, modifier: Modifier = Modifier, + contentDescription: String? = null, ) { AsyncImage( model = avatarData, onError = { Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable) }, - contentDescription = null, + contentDescription = contentDescription, contentScale = ContentScale.Crop, placeholder = debugPlaceholderAvatar(), modifier = modifier @@ -89,7 +95,7 @@ private fun InitialsAvatar( end = Offset(100f, 0f) ) Box( - modifier.background(brush = initialsGradient) + modifier.background(brush = initialsGradient), ) { Text( modifier = Modifier.align(Alignment.Center), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt new file mode 100644 index 0000000000..a18d0ef3ed --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/ApplyIf.kt @@ -0,0 +1,45 @@ +/* + * 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.designsystem.modifiers + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.platform.debugInspectorInfo + +/** + * Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise. + */ +@SuppressLint("UnnecessaryComposedModifier") // It's actually necessary due to the `@Composable` lambdas +fun Modifier.applyIf( + condition: Boolean, + ifTrue: @Composable Modifier.() -> Modifier, + ifFalse: @Composable (Modifier.() -> Modifier)? = null +): Modifier = + composed( + inspectorInfo = debugInspectorInfo { + name = "applyIf" + value = condition + } + ) { + when { + condition -> then(ifTrue(Modifier)) + ifFalse != null -> then(ifFalse(Modifier)) + else -> this + } + } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt new file mode 100644 index 0000000000..9675c54a20 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt @@ -0,0 +1,106 @@ +/* + * 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.designsystem.modifiers + +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.platform.debugInspectorInfo +import kotlin.math.sqrt + +// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8 + +/** + * A modifier that clips the composable content using an animated circle. The circle will + * expand/shrink with an animation whenever [visible] changes. + * + * For more fine-grained control over the transition, see this method's overload, which allows passing + * a [State] object to control the progress of the reveal animation. + * + * By default, the circle is centered in the content, but custom positions may be specified using + * [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/ +fun Modifier.circularReveal( + visible: Boolean, + showScrim: Boolean = false, + revealFrom: Offset = Offset(0.5f, 0.5f), +): Modifier = composed( + factory = { + val factor = updateTransition(visible, label = "Visibility") + .animateFloat(label = "revealFactor") { if (it) 1f else 0f } + + circularReveal(factor, showScrim, revealFrom) + }, + inspectorInfo = debugInspectorInfo { + name = "circularReveal" + properties["visible"] = visible + properties["revealFrom"] = revealFrom + } +) + +/** + * A modifier that clips the composable content using a circular shape. The radius of the circle + * will be determined by the [transitionProgress]. + * + * The values of the progress should be between 0 and 1. + * + * By default, the circle is centered in the content, but custom positions may be specified using + * [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom). + * */ +fun Modifier.circularReveal( + transitionProgress: State, + showScrim: Boolean = false, + revealFrom: Offset = Offset(0.5f, 0.5f) +): Modifier { + return drawWithCache { + val path = Path() + val center = revealFrom.mapTo(size) + val radius = calculateRadius(revealFrom, size) + val scrimColor = if (showScrim) + Color.Gray + else + Color.Transparent + + path.addOval(Rect(center, radius * transitionProgress.value)) + + onDrawWithContent { + if (showScrim) { + drawRect(scrimColor, alpha = transitionProgress.value * 0.75f) + } + clipPath(path) { this@onDrawWithContent.drawContent() } + } + } +} + +private fun Offset.mapTo(size: Size): Offset { + return Offset(x * size.width, y * size.height) +} + +private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) { + val x = (if (x > 0.5f) x else 1 - x) * size.width + val y = (if (y > 0.5f) y else 1 - y) * size.height + + sqrt(x * x + y * y) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt new file mode 100644 index 0000000000..33baf19dce --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowInsetsExtension.kt @@ -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.designsystem.utils + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection + +@Composable +fun WindowInsets.copy( + top: Int? = null, + right: Int? = null, + bottom: Int? = null, + left: Int? = null +): WindowInsets { + val density = LocalDensity.current + val direction = LocalLayoutDirection.current + return WindowInsets( + top = top ?: this.getTop(density), + right = right ?: this.getRight(density, direction), + bottom = bottom ?: this.getBottom(density), + left = left ?: this.getLeft(density, direction) + ) +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png index 167599e090..ec4ab9f3e7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bca09418758a20493dae2e73a747449af8a448ad3a3cc4c5aae2e08a425f3fb -size 13464 +oid sha256:b5f2b24a19ca49b3e6e34ccd65d2bdba72d0384104931bda92e191959d58c5c3 +size 12697 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png index 7efc250776..ddbf6df0b5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl.components_null_DefaultGroup_DefaultRoomListTopBarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e50325c75193e47958862ea9cb515d7c84d2c47a00b01256fc244319780c107f -size 12425 +oid sha256:c4fa32eb24a0cc51b9b19c6f24a7d3d59aae65f1f30a43b1a6d70b3ed3e2154d +size 11716 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b113ba20f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListSearchResultContentPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0 +size 27758 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 4e36616412..e4b5f95b8d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf -size 37781 +oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288 +size 37044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 0d3bec9cf5..1dcb6593b1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d729c75d73d1837b365c8332b6e4203cb2492f6c5c4af06741a4bd2e818daebb -size 60667 +oid sha256:8b265978c4db7b266fd07d56364eccafba1cd765ed9bf6d5a03b1584e173ba6a +size 59936 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 4e36616412..e4b5f95b8d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fc272268179409483fc5fad89aa00714a6811c63e478b5c696d4ab0338ce6bf -size 37781 +oid sha256:3cdb131c68de1fce5a3319151e39148e9f3a71c7bc3984e89ec0a80abf0f7288 +size 37044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index c454377b9b..52c50a59a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56a253a7981823ca0fc7b653f9e83d29d1f259fea43ca4cee5760fa863306f2c -size 39847 +oid sha256:ccf989dac7fad3cc70443d96e1ebd519463a6559ed0795ae2a1ffbaf91bdfe7c +size 39092 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 2063e4eeee..e7c9ed7df3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b1222e1ef2d0739caa410540bf74fea681ef2e05bb04b976e50f1fe5256d613 -size 39762 +oid sha256:c2a23141c6cc8aa6e7e5f0757bda4d1117bf7f752412ccc4649ea560113b3e3f +size 39030 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..069af4cecf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9095546c30bb5bc9800c852456fa9cd82d14e873a5e1488d29496af088e951da +size 4882 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9861b7572e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cfe321535e1ce223a0460435123dc59e74c436bdd8696cf4bdb2169f511832b +size 28541 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 4a51d9cddc..2aa0933d88 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43b69859fa3ee38d2b7f7415b87738db65dc6dac3d2fabddc1f1346b0b64932b -size 37329 +oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60 +size 36658 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 92729ff9ed..2fcea72749 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f61d4fc4d5af166e1761f004e7d80934eef24a582c96c590c769ed1fb13041a4 -size 59489 +oid sha256:0cdd1fad4b3db78fb8599785334366e3bfbaf5dead7990f396f8752862ab9e98 +size 58987 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 4a51d9cddc..2aa0933d88 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43b69859fa3ee38d2b7f7415b87738db65dc6dac3d2fabddc1f1346b0b64932b -size 37329 +oid sha256:a2a261b30866af95b856ee1e7d6ac2cbe2d638cf80277645f361b043d2f94e60 +size 36658 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 1287c0ec4f..2664376698 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bf9529823b4e04261b1528b2309c549f1d3aad80c542059f11c1b26a2979b04 -size 39359 +oid sha256:1ba3d8a5cfbd102dc6df8f511eb14663170a6d550ee5e65702bc1f3fce3efd14 +size 38657 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 273e7af0cf..5793650c81 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebb15a58e5d3497b2819f5a6b6fde88962f01cc2ae02d4b8cda0e45e1dde677f -size 39314 +oid sha256:d427b479f9eb6227bb92aecd997bab97a735ef85dd279a575d777e754effd258 +size 38639 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6c14f8062c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d463ab6d045cc310973c5ff900cf4d9ae04e93cb1c7eac3f9b2aa0ea9b827cee +size 4815 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b113ba20f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0 +size 27758 From 40f927fbdf86333d28d3b61d4b285924e7a4a57d Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Tue, 25 Apr 2023 13:06:31 +0100 Subject: [PATCH 13/33] Update labelled issue automation Migrate from graphql to actions. Add QA team --- .github/workflows/triage-labelled.yml | 67 ++++++++------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 136acfe491..d7951a012b 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -12,22 +12,10 @@ jobs: if: > github.repository == 'vector-im/element-x-android' steps: - - uses: octokit/graphql-action@v2.x + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ABTXY" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/43 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ex_plorers: name: Add labelled issues to X-Plorer project @@ -35,23 +23,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Element X Feature') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/73 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} verticals_feature: name: Add labelled issues to Verticals Feature project @@ -59,20 +34,18 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/57 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + qa: + name: Add labelled issues to QA project + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: QA') + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/vector-im/projects/69 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} From 0389f782c94feedfe447d2f9c1f903a0e50c60f6 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 25 Apr 2023 18:01:54 +0200 Subject: [PATCH 14/33] Fix: Maestro tests fail when using settings (#358) * Fix Maestro tests using 'Settings' to open the settings screen. * Try to allow manually running Maestro tests. * Also adjust logout flow. --- .github/workflows/maestro.yml | 2 +- .maestro/tests/account/logout.yaml | 3 ++- .maestro/tests/settings/settings.yaml | 3 ++- .../features/roomlist/impl/components/RoomListTopBar.kt | 9 +++++++-- .../io/element/android/libraries/testtags/TestTags.kt | 5 +++++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 72f9aee8f1..13e0c3b4ad 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -16,7 +16,7 @@ jobs: maestro-cloud: name: Maestro test suite runs-on: ubuntu-latest - if: github.event.review.state == 'approved' + if: github.event.review.state == 'approved' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false # Allow one per PR. diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index 3c9dd07972..a06ac25e2d 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -1,6 +1,7 @@ appId: ${APP_ID} --- -- tapOn: "Settings" +- tapOn: + id: "home_screen-settings" - tapOn: "Sign out" - takeScreenshot: build/maestro/900-SignOutDialg # Ensure cancel cancels diff --git a/.maestro/tests/settings/settings.yaml b/.maestro/tests/settings/settings.yaml index 397a0f70b5..ee3104024c 100644 --- a/.maestro/tests/settings/settings.yaml +++ b/.maestro/tests/settings/settings.yaml @@ -1,6 +1,7 @@ appId: ${APP_ID} --- -- tapOn: "Settings" +- tapOn: + id: "home_screen-settings" - assertVisible: "Rageshake to report bug" - takeScreenshot: build/maestro/600-Settings - tapOn: diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 48c8ac0253..11f0616e07 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -45,6 +45,8 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @@ -101,14 +103,17 @@ private fun DefaultRoomListTopBar( }, navigationIcon = { if (matrixUser != null) { - IconButton(onClick = onOpenSettings) { + IconButton( + modifier = Modifier.testTag(TestTags.homeScreenSettings), + onClick = onOpenSettings + ) { Avatar(matrixUser.avatarData, contentDescription = stringResource(StringR.string.common_settings)) } } }, actions = { IconButton( - onClick = onSearchClicked + onClick = onSearchClicked, ) { Icon(Icons.Default.Search, contentDescription = stringResource(StringR.string.action_search)) } diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index a254a636ef..df12c755e3 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -38,6 +38,11 @@ object TestTags { */ val changeServerServer = TestTag("change_server-server") val changeServerContinue = TestTag("change_server-continue") + + /** + * Room list / Home screen. + */ + val homeScreenSettings = TestTag("home_screen-settings") } From 5e8636d66e68fe358ba1e7005fa8b6f2c16198a7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Apr 2023 18:26:18 +0200 Subject: [PATCH 15/33] Update kotlinc.xml with kotlin 1.8.20 --- .idea/kotlinc.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 0fc3113136..69e86158ba 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From dc5a446b5cd8a3e9b0e8aa8fe4782e173c53ee5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 19:36:57 +0000 Subject: [PATCH 16/33] Update plugin ktlint to v11.3.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 275145a865..cdabcd80e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -164,7 +164,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.3.1" +ktlint = "org.jlleitschuh.gradle.ktlint:11.3.2" dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } paparazzi = "app.cash.paparazzi:1.2.0" From 2376d32b9e25873e6385601c7577e044346b7417 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 26 Apr 2023 16:14:44 +0200 Subject: [PATCH 17/33] [Room Details] Block & unblock user (#340) --- .../io/element/android/appnav/RoomFlowNode.kt | 11 +++ changelog.d/339.feature | 1 + .../impl/timeline/TimelinePresenter.kt | 7 -- .../timeline/TimelinePresenterTest.kt | 17 ---- .../roomdetails/blockuser/BlockUserSection.kt | 89 ++++++++++++++++++ .../roomdetails/impl/RoomDetailsNode.kt | 18 +++- .../roomdetails/impl/RoomDetailsPresenter.kt | 49 +++++++--- .../roomdetails/impl/RoomDetailsState.kt | 5 +- .../impl/RoomDetailsStateProvider.kt | 1 + .../roomdetails/impl/RoomDetailsView.kt | 12 +-- .../roomdetails/impl/di/RoomMemberModules.kt | 14 +-- .../impl/members/RoomMemberListNode.kt | 8 +- .../impl/members/RoomUserListDataSource.kt | 3 +- .../details/RoomMemberDetailsEvents.kt | 6 +- .../members/details/RoomMemberDetailsNode.kt | 2 - .../details/RoomMemberDetailsPresenter.kt | 53 +++++++++-- .../members/details/RoomMemberDetailsState.kt | 10 ++- .../details/RoomMemberDetailsStateProvider.kt | 5 +- .../members/details/RoomMemberDetailsView.kt | 28 ++---- .../roomdetails/RoomDetailsPresenterTests.kt | 42 ++++++--- .../RoomMemberDetailsPresenterTests.kt | 68 +++++++++++++- .../libraries/core/coroutine/ErrorFlow.kt | 22 +++++ .../libraries/matrix/api/room/MatrixRoom.kt | 12 ++- .../matrix/impl/room/RustMatrixRoom.kt | 90 +++++++++++-------- .../impl/timeline/RustMatrixTimeline.kt | 9 +- libraries/matrix/test/build.gradle.kts | 1 + .../matrix/test/room/FakeMatrixRoom.kt | 40 ++++++--- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...lsDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...lsDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...sLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...sLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- 35 files changed, 477 insertions(+), 174 deletions(-) create mode 100644 changelog.d/339.feature create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt create mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 22929e3d66..f4bf3465cb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -18,6 +18,7 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children @@ -134,8 +135,18 @@ class RoomFlowNode @AssistedInject constructor( object RoomDetails : NavTarget } + private val timeline = inputs.room.timeline() + @Composable override fun View(modifier: Modifier) { + + DisposableEffect(Unit) { + timeline.initialize() + onDispose { + timeline.dispose() + } + } + Children( navModel = backstack, modifier = modifier, diff --git a/changelog.d/339.feature b/changelog.d/339.feature new file mode 100644 index 0000000000..4cbf834b1c --- /dev/null +++ b/changelog.d/339.feature @@ -0,0 +1 @@ +Block & unblock users from room details screen. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 1d931c76bb..9418e59edc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -80,13 +80,6 @@ class TimelinePresenter @Inject constructor( .launchIn(this) } - DisposableEffect(Unit) { - timeline.initialize() - onDispose { - timeline.dispose() - } - } - return TimelineState( highlightedEventId = highlightedEventId.value, paginationState = paginationState.value, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 24e6f77d32..41f37158d9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -49,23 +49,6 @@ class TimelinePresenterTest { } } - @Test - fun `present - makes sure timeline is initialized and disposed`() = runTest { - val fakeTimeline = FakeMatrixTimeline() - val presenter = TimelinePresenter( - timelineItemsFactory = aTimelineItemsFactory(), - room = FakeMatrixRoom(matrixTimeline = fakeTimeline), - ) - assertThat(fakeTimeline.isInitialized).isFalse() - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - skipItems(2) - assertThat(fakeTimeline.isInitialized).isTrue() - } - assertThat(fakeTimeline.isInitialized).isFalse() - } - @Test fun `present - load more`() = runTest { val presenter = TimelinePresenter( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt new file mode 100644 index 0000000000..49daa15af3 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/blockuser/BlockUserSection.kt @@ -0,0 +1,89 @@ +/* + * 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.features.roomdetails.blockuser + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Block +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.theme.LocalColors + +@Composable +internal fun BlockUserSection(state: RoomMemberDetailsState, modifier: Modifier = Modifier) { + PreferenceCategory(showDivider = false, modifier = modifier) { + if (state.isBlocked) { + PreferenceText( + title = stringResource(R.string.screen_dm_details_unblock_user), + icon = Icons.Outlined.Block, + onClick = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) }, + ) + } else { + PreferenceText( + title = stringResource(R.string.screen_dm_details_block_user), + icon = Icons.Outlined.Block, + tintColor = LocalColors.current.textActionCritical, + onClick = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) }, + ) + } + } +} + +@Composable +internal fun BlockUserDialogs(state: RoomMemberDetailsState) { + when (state.displayConfirmationDialog) { + null -> Unit + RoomMemberDetailsState.ConfirmationDialog.Block -> { + BlockConfirmationDialog( + onBlockAction = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) }, + onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } + ) + } + RoomMemberDetailsState.ConfirmationDialog.Unblock -> { + UnblockConfirmationDialog( + onUnblockAction = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) }, + onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } + ) + } + } +} + +@Composable +internal fun BlockConfirmationDialog(onBlockAction: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + content = stringResource(R.string.screen_dm_details_block_alert_description), + submitText = stringResource(R.string.screen_dm_details_block_alert_action), + onSubmitClicked = onBlockAction, + onDismiss = onDismiss + ) +} + +@Composable +internal fun UnblockConfirmationDialog(onUnblockAction: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + content = stringResource(R.string.screen_dm_details_unblock_alert_description), + submitText = stringResource(R.string.screen_dm_details_unblock_alert_action), + onSubmitClicked = onUnblockAction, + onDismiss = onDismiss + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 46e76a5f8a..37110e5192 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -63,7 +63,10 @@ class RoomDetailsNode @AssistedInject constructor( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, + noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) ) + }.onFailure { + Timber.e(it) } } @@ -86,12 +89,21 @@ class RoomDetailsNode @AssistedInject constructor( override fun View(modifier: Modifier) { val context = LocalContext.current val state = presenter.present() + + fun onShareRoom() { + this.onShareRoom(context) + } + + fun onShareMember(roomMember: RoomMember) { + this.onShareMember(context, roomMember) + } + RoomDetailsView( state = state, modifier = modifier, - goBack = { navigateUp() }, - onShareRoom = { onShareRoom(context) }, - onShareMember = { onShareMember(context, it) }, + goBack = this::navigateUp, + onShareRoom = ::onShareRoom, + onShareMember = ::onShareMember, openRoomMemberList = ::openRoomMemberList, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 45e978b80a..42a392a900 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,12 +17,14 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient @@ -30,16 +32,32 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.runBlocking import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( - private val sessionId: SessionId, + private val matrixClient: MatrixClient, private val room: MatrixRoom, private val roomMembershipObserver: RoomMembershipObserver, ) : Presenter { + private val roomMemberDetailsPresenter by lazy { + val dmMember = runBlocking { + room.getDmMember().firstOrNull() + } + if (dmMember != null) { + RoomMemberDetailsPresenter(matrixClient.sessionId, room, dmMember) + } else { + null + } + } + @Composable override fun present(): RoomDetailsState { val coroutineScope = rememberCoroutineScope() @@ -50,20 +68,16 @@ class RoomDetailsPresenter @Inject constructor( mutableStateOf(null) } - var memberCount: Async by remember { mutableStateOf(Async.Loading()) } - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - memberCount = runCatching { room.memberCount() } - .fold( - onSuccess = { Async.Success(it) }, - onFailure = { Async.Failure(it) } - ) - } + val memberCount by produceState>(initialValue = Async.Loading(null)) { + room.members().map { it.count() } + .onEach { value = Async.Success(it) } + .catch { value = Async.Failure(it) } + .launchIn(coroutineScope) } - val dmMember = room.getDmMember() + val dmMember by room.getDmMember().collectAsState(initial = null) val roomType = if (dmMember != null) { - RoomDetailsType.Dm(dmMember) + RoomDetailsType.Dm(dmMember!!) } else { RoomDetailsType.Room } @@ -90,6 +104,12 @@ class RoomDetailsPresenter @Inject constructor( } } + val roomMemberDetailsState = if (dmMember != null) { + roomMemberDetailsPresenter?.present() + } else { + null + } + return RoomDetailsState( roomId = room.roomId.value, roomName = room.name ?: room.displayName, @@ -101,6 +121,7 @@ class RoomDetailsPresenter @Inject constructor( displayLeaveRoomWarning = leaveRoomWarning, error = error, roomType = roomType, + roomMemberDetailsState = roomMemberDetailsState, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index f8fed122de..173ba66ed0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -16,10 +16,8 @@ package io.element.android.features.roomdetails.impl +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.isLoading - -import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember data class RoomDetailsState( @@ -33,6 +31,7 @@ data class RoomDetailsState( val displayLeaveRoomWarning: LeaveRoomWarning?, val error: RoomDetailsError?, val roomType: RoomDetailsType, + val roomMemberDetailsState: RoomMemberDetailsState?, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 61b9d310af..d30ea15f4a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -71,5 +71,6 @@ fun aRoomDetailsState() = RoomDetailsState( displayLeaveRoomWarning = null, error = null, roomType = RoomDetailsType.Room, + roomMemberDetailsState = null, eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index b9266cd8ca..9b3f83d456 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -42,7 +42,8 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.roomdetails.impl.members.details.BlockSection +import io.element.android.features.roomdetails.blockuser.BlockUserDialogs +import io.element.android.features.roomdetails.blockuser.BlockUserSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection import io.element.android.features.roomdetails.impl.members.details.RoomMemberShareSection import io.element.android.libraries.architecture.Async @@ -135,10 +136,11 @@ fun RoomDetailsView( }) } is RoomDetailsType.Dm -> { - BlockSection( - isBlocked = state.roomType.roomMember.isIgnored, - onToggleBlock = { /*TODO*/ } - ) + if (state.roomMemberDetailsState != null) { + val roomMemberState = state.roomMemberDetailsState + BlockUserSection(roomMemberState) + BlockUserDialogs(roomMemberState) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt index 68f77b5821..b17cb9ab6a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt @@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module import dagger.Provides -import io.element.android.features.roomdetails.impl.RoomDetailsPresenter import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.userlist.api.UserListDataSource @@ -28,7 +27,6 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import javax.inject.Named @Module @@ -44,22 +42,14 @@ interface RoomMemberBindsModule { @ContributesTo(RoomScope::class) object RoomMemberProvidesModule { - @Provides - fun provideRoomDetailsPresenter( - matrixClient: MatrixClient, - room: MatrixRoom, - roomMembershipObserver: RoomMembershipObserver, - ): RoomDetailsPresenter { - return RoomDetailsPresenter(matrixClient.sessionId, room, roomMembershipObserver) - } - @Provides fun provideRoomMemberDetailsPresenterFactory( + matrixClient: MatrixClient, room: MatrixRoom, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(room, roomMember) + return RoomMemberDetailsPresenter(matrixClient.sessionId, room, roomMember) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index fafe0ede99..aacc0113c9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -29,6 +29,9 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch import timber.log.Timber @ContributesNode(RoomScope::class) @@ -37,6 +40,7 @@ class RoomMemberListNode @AssistedInject constructor( @Assisted plugins: List, private val room: MatrixRoom, private val presenter: RoomMemberListPresenter, + private val coroutineScope: CoroutineScope, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { @@ -45,8 +49,8 @@ class RoomMemberListNode @AssistedInject constructor( private val callbacks = plugins() - private fun onUserSelected(matrixUser: MatrixUser) { - val member = room.getMember(matrixUser.id) + private fun onUserSelected(matrixUser: MatrixUser) = coroutineScope.launch { + val member = room.getMember(matrixUser.id).firstOrNull() if (member != null) { callbacks.forEach { it.openRoomMemberDetails(member) } } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index dc0008d2a3..dc74a9a809 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject class RoomUserListDataSource @Inject constructor( @@ -30,7 +31,7 @@ class RoomUserListDataSource @Inject constructor( ) : UserListDataSource { override suspend fun search(query: String): List { - return room.members().filter { member -> + return room.members().firstOrNull().orEmpty().filter { member -> if (query.isBlank()) { true } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt index 2c74caa8fd..5848561f3e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt @@ -16,4 +16,8 @@ package io.element.android.features.roomdetails.impl.members.details -sealed interface RoomMemberDetailsEvents +sealed interface RoomMemberDetailsEvents { + data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents + data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents + object ClearConfirmationDialog : RoomMemberDetailsEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 5cd2544537..72e335c1d2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -30,7 +30,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.room.RoomMember import timber.log.Timber @@ -52,7 +51,6 @@ class RoomMemberDetailsNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val context = LocalContext.current fun onShareUser() { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index de24b5ee1b..d8b317d75a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -17,15 +17,26 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( + private val currentUserSessionId: SessionId, private val room: MatrixRoom, @Assisted private val roomMember: RoomMember, ) : Presenter { @@ -36,11 +47,31 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @Composable override fun present(): RoomMemberDetailsState { + val coroutineScope = rememberCoroutineScope() + var confirmationDialog by remember { mutableStateOf(null) } + var isBlocked = remember { mutableStateOf(roomMember.isIgnored) } -// fun handleEvents(event: RoomMemberDetailsEvents) { -// when (event) { -// } -// } + fun handleEvents(event: RoomMemberDetailsEvents) { + when (event) { + is RoomMemberDetailsEvents.BlockUser -> { + if (event.needsConfirmation) { + confirmationDialog = ConfirmationDialog.Block + } else { + confirmationDialog = null + coroutineScope.blockUser(roomMember.userId, isBlocked) + } + } + is RoomMemberDetailsEvents.UnblockUser -> { + if (event.needsConfirmation) { + confirmationDialog = ConfirmationDialog.Unblock + } else { + confirmationDialog = null + coroutineScope.unblockUser(roomMember.userId, isBlocked) + } + } + RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null + } + } val userName by produceState(initialValue = roomMember.displayName) { room.userDisplayName(roomMember.userId).onSuccess { displayName -> @@ -58,8 +89,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( userId = roomMember.userId.value, userName = userName, avatarUrl = userAvatar, - isBlocked = roomMember.isIgnored, -// eventSink = ::handleEvents + isBlocked = isBlocked.value, + displayConfirmationDialog = confirmationDialog, + isCurrentUser = roomMember.userId == currentUserSessionId, + eventSink = ::handleEvents ) } + + private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState) = launch { + room.ignoreUser(userId).onSuccess { isBlockedState.value = true } + } + + private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState) = launch { + room.unignoreUser(userId).onSuccess { isBlockedState.value = false } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt index d9e3f949e7..0a2895db09 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt @@ -21,5 +21,11 @@ data class RoomMemberDetailsState( val userName: String?, val avatarUrl: String?, val isBlocked: Boolean, -// val eventSink: (RoomMemberDetailsEvents) -> Unit -) + val displayConfirmationDialog: ConfirmationDialog? = null, + val isCurrentUser: Boolean, + val eventSink: (RoomMemberDetailsEvents) -> Unit +) { + enum class ConfirmationDialog { + Block, Unblock + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt index c719ab7a26..d8e7ce5ad3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -24,6 +24,8 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider Unit, modifier: Modifier = } } -@Composable -internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) { - PreferenceCategory(showDivider = false, modifier = modifier) { - if (isBlocked) { - PreferenceText( - title = stringResource(R.string.screen_dm_details_unblock_user), - icon = Icons.Outlined.Block, - ) - } else { - PreferenceText( - title = stringResource(R.string.screen_dm_details_block_user), - icon = Icons.Outlined.Block, - tintColor = LocalColors.current.textActionCritical, - ) - } - } -} - @Preview @Composable fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 66e74cd5cc..d6bf99472d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth import io.element.android.features.roomdetails.impl.LeaveRoomWarning import io.element.android.features.roomdetails.impl.RoomDetailsEvent import io.element.android.features.roomdetails.impl.RoomDetailsPresenter +import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -32,8 +33,8 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect @@ -50,7 +51,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -69,7 +70,7 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -84,7 +85,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -95,12 +96,33 @@ class RoomDetailsPresenterTests { } } + @Test + fun `present - initial state with DM member sets custom DM roomType`() = runTest { + val room = aMatrixRoom(name = null).apply { + givenDmMember(aRoomMember()) + } + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // It's not configured yet in the first iteration + Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Room) + + // Once updated, the RoomDetailsType becomes 'Dm' + val updatedState = awaitItem() + Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(aRoomMember())) + + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - can handle error while fetching member count`() = runTest { val room = aMatrixRoom(name = null).apply { givenFetchMemberResult(Result.failure(Throwable())) } - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -114,7 +136,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { val room = aMatrixRoom(isPublic = false) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -131,7 +153,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest { val room = aMatrixRoom(members = listOf(aRoomMember())) - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -148,7 +170,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave with confirmation shows a generic warning`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -165,7 +187,7 @@ class RoomDetailsPresenterTests { @Test fun `present - Leave without confirmation leaves the room`() = runTest { val room = aMatrixRoom() - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -189,7 +211,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenLeaveRoomError(Throwable()) } - val presenter = RoomDetailsPresenter(A_SESSION_ID, room, roomMembershipObserver) + val presenter = RoomDetailsPresenter(FakeMatrixClient(), room, roomMembershipObserver) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index 68de573fae..a98ec1f971 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -22,7 +22,10 @@ import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.aRoomMember +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.libraries.matrix.test.A_SESSION_ID import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,7 +40,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success("A custom avatar")) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(room, roomMember) + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -60,7 +63,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.failure(Throwable())) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(room, roomMember) + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -79,7 +82,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success(null)) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(room, roomMember) + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -90,4 +93,63 @@ class RoomMemberDetailsPresenterTests { ensureAllEventsConsumed() } } + + @Test + fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember() + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) + + val dialogState = awaitItem() + Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Block) + + dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) + Truth.assertThat(awaitItem().displayConfirmationDialog).isNull() + + ensureAllEventsConsumed() + } + } + + @Test + fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember() + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) + Truth.assertThat(awaitItem().isBlocked).isTrue() + + initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) + Truth.assertThat(awaitItem().isBlocked).isFalse() + } + } + + @Test + fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember() + val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) + + val dialogState = awaitItem() + Truth.assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Unblock) + + dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) + Truth.assertThat(awaitItem().displayConfirmationDialog).isNull() + + ensureAllEventsConsumed() + } + } } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt new file mode 100644 index 0000000000..302978066c --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/ErrorFlow.kt @@ -0,0 +1,22 @@ +/* + * 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.flow.flow + +/** Create a Flow emitting a single error event. It should be useful for tests. */ +fun errorFlow(throwable: Throwable) = flow { throw throwable } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 70980cc753..4682e88bf9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -36,13 +36,13 @@ interface MatrixRoom : Closeable { val isDirect: Boolean val isPublic: Boolean - suspend fun members(): List + fun members() : Flow> - suspend fun memberCount(): Int + fun updateMembers() - fun getMember(userId: UserId): RoomMember? + fun getMember(userId: UserId): Flow - fun getDmMember(): RoomMember? + fun getDmMember(): Flow fun syncUpdateFlow(): Flow @@ -62,6 +62,10 @@ interface MatrixRoom : Closeable { suspend fun redactEvent(eventId: EventId, reason: String? = null): Result + suspend fun ignoreUser(userId: UserId): Result + + suspend fun unignoreUser(userId: UserId): Result + suspend fun leave(): Result suspend fun acceptInvitation(): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 8afa3cb4ed..90867ccc3f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -26,18 +26,19 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember class RustMatrixRoom( private val currentUserId: UserId, @@ -48,37 +49,40 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { - private var loadMembersJob: Job? = null - private var cachedMembers: List = emptyList() + private val timeline by lazy { + RustMatrixTimeline( + matrixRoom = this, + innerRoom = innerRoom, + slidingSyncRoom = slidingSyncRoom, + coroutineScope = coroutineScope, + coroutineDispatchers = coroutineDispatchers + ) + } - override suspend fun members(): List { - return cachedMembers.ifEmpty { - if (loadMembersJob == null) { - loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) { - cachedMembers = tryOrNull { - innerRoom.members().map(RoomMemberMapper::map) - } ?: emptyList() - } + private var membersFlow = MutableStateFlow>(emptyList()) + + override fun members(): Flow> { + return membersFlow.onSubscription { updateMembers() } + } + + override fun updateMembers() { + val updatedMembers = tryOrNull { + innerRoom.members().map(RoomMemberMapper::map) + } ?: emptyList() + membersFlow.tryEmit(updatedMembers) + } + + override fun getMember(userId: UserId): Flow { + return membersFlow.map { members -> members.find { it.userId == userId } } + } + + override fun getDmMember(): Flow { + return membersFlow.map { members -> + if (members.size == 2 && isDirect && isEncrypted) { + members.find { it.userId != currentUserId } + } else { + null } - loadMembersJob?.join() - loadMembersJob = null - cachedMembers - } - } - - override suspend fun memberCount(): Int { - return members().size - } - - override fun getMember(userId: UserId): RoomMember? { - return cachedMembers.find { it.userId == userId } - } - - override fun getDmMember(): RoomMember? { - return if (cachedMembers.size == 2 && isDirect && isEncrypted) { - cachedMembers.find { it.userId != currentUserId } - } else { - null } } @@ -94,13 +98,7 @@ class RustMatrixRoom( } override fun timeline(): MatrixTimeline { - return RustMatrixTimeline( - matrixRoom = this, - innerRoom = innerRoom, - slidingSyncRoom = slidingSyncRoom, - coroutineScope = coroutineScope, - coroutineDispatchers = coroutineDispatchers - ) + return timeline } override fun close() { @@ -219,4 +217,20 @@ class RustMatrixRoom( } } + + override suspend fun ignoreUser(userId: UserId): Result { + return runCatching { + getRustMember(userId)?.ignore() ?: error("No member with userId $userId exists in room $roomId") + } + } + + override suspend fun unignoreUser(userId: UserId): Result { + return runCatching { + getRustMember(userId)?.unignore() ?: error("No member with userId $userId exists in room $roomId") + } + } + + private fun getRustMember(userId: UserId): RustRoomMember? { + return innerRoom.members().find { it.userId() == userId.value } + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index e942f31b76..ac43eaa965 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -139,7 +139,14 @@ class RustMatrixTimeline( private suspend fun addListener(timelineListener: TimelineListener): Result> = withContext(coroutineDispatchers.io) { runCatching { - val settings = RoomSubscription(requiredState = listOf(RequiredState(key = "m.room.canonical_alias", value = "")), timelineLimit = null) + val settings = RoomSubscription( + requiredState = listOf( + RequiredState(key = "m.room.topic", value = ""), + RequiredState(key = "m.room.canonical_alias", value = ""), + RequiredState(key = "m.room.join_rules", value = ""), + ), + timelineLimit = null + ) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) listenerTokens += result.taskHandle result.items diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index 9f1d35112f..6d9ca1eb8e 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -23,6 +23,7 @@ android { } dependencies { + api(projects.libraries.core) api(projects.libraries.matrix.api) api(libs.coroutines.core) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 05890f9e4a..8e604016cf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.test.room +import io.element.android.libraries.core.coroutine.errorFlow import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -27,6 +28,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf class FakeMatrixRoom( override val roomId: RoomId = A_ROOM_ID, @@ -50,6 +52,8 @@ class FakeMatrixRoom( private var rejectInviteResult = Result.success(Unit) private var dmMember: RoomMember? = null private var fetchMemberResult: Result = Result.success(Unit) + private var ignoreResult = Result.success(Unit) + private var unignoreResult = Result.success(Unit) var areMembersFetched: Boolean = false private set @@ -78,8 +82,8 @@ class FakeMatrixRoom( } } - override fun getDmMember(): RoomMember? { - return dmMember + override fun getDmMember(): Flow { + return flowOf(dmMember) } override suspend fun userDisplayName(userId: UserId): Result { @@ -90,20 +94,18 @@ class FakeMatrixRoom( return userAvatarUrlResult } - override suspend fun members(): List { - return members + override fun members(): Flow> { + return fetchMemberResult.fold(onSuccess = { + flowOf(members) + }, onFailure = { + errorFlow(it) + }) } - override suspend fun memberCount(): Int { - if (fetchMemberResult.isSuccess) { - return members.count() - } else { - throw fetchMemberResult.exceptionOrNull()!! - } - } + override fun updateMembers() = Unit - override fun getMember(userId: UserId): RoomMember? { - return members.firstOrNull { it.userId == userId } + override fun getMember(userId: UserId): Flow { + return flowOf(members.find { it.userId == userId }) } override suspend fun sendMessage(message: String): Result { @@ -138,6 +140,10 @@ class FakeMatrixRoom( return Result.success(Unit) } + override suspend fun ignoreUser(userId: UserId): Result = ignoreResult + + override suspend fun unignoreUser(userId: UserId): Result = unignoreResult + override suspend fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) override suspend fun acceptInvitation(): Result { isInviteAccepted = true @@ -179,4 +185,12 @@ class FakeMatrixRoom( rejectInviteResult = result } + + fun givenIgnoreResult(result: Result) { + ignoreResult = result + } + + fun givenUnIgnoreResult(result: Result) { + unignoreResult = result + } } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..de77e469f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d +size 28744 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..de77e469f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8c78efed997712873719636ff1f8479d38d317e443c5d4340346d4328de9c0d +size 28744 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f269338d7c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff +size 28303 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f269338d7c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f62537b4aa79f501908d1fff9d269139e88db5f6dbaedcf63670a4f71b47bff +size 28303 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png index e9e352cb20..4ec6a0f362 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:354452861d1e006a8bfa744251ffdaf15088e0bb181a53043f121e606233d648 -size 67340 +oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842 +size 64550 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png index 8dd1cd9116..4ec6a0f362 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1cb6bac9e72b956d0cffde807340e36a7a4d6873d4d7337995b53e82769c4f9 -size 68135 +oid sha256:a8a9186d741d251dc8bdf6bcf71d395e8c057c310585aeb8911609eaaacce842 +size 64550 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png index 815e64ba9f..562eb63427 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc56e29c26250c6fef312ec4c5fdfaa2f63159fd0565bd37522b46c7ff67906a -size 61924 +oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8 +size 58643 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png index 687b034bee..562eb63427 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6faff25a1c187dee59d0dcf0affd5029251512efb6eff7fc2d41d34bace2061 -size 62356 +oid sha256:94a589b556a750485fd61af6446457c98c5f112d0d013cd78016b88a8829e6a8 +size 58643 From 78a715ce8d80c40dde01f7269c33bfccdc6324b4 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 27 Apr 2023 12:46:32 +0200 Subject: [PATCH 18/33] Fix `NotificationData?.orDefault` using an invalid UserId (#362) --- .../push/impl/notifications/NotifiableEventResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 3c82496626..242ca6a67d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -119,7 +119,7 @@ private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): Notif isRemote = false, localSendState = null, reactions = emptyList(), - sender = UserId(""), + sender = UserId("@user:domain"), senderProfile = ProfileTimelineDetails.Unavailable, timestamp = System.currentTimeMillis(), content = MessageContent( From b433725783d79a4e448d85acdea596deeb80b7af Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 Apr 2023 17:34:27 +0200 Subject: [PATCH 19/33] RoomDetailsPresenter: fix dm test --- .../roomdetails/RoomDetailsPresenterTests.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index acac5e7ccc..949cf269d4 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -113,15 +113,13 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with DM member sets custom DM roomType`() = runTest { + val myRoomMember = aRoomMember(A_SESSION_ID) + val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aMatrixRoom( isEncrypted = true, - isPublic = false, - name = null + isDirect = true, ).apply { - val roomMembers = listOf( - aRoomMember(A_SESSION_ID), - aRoomMember(A_USER_ID_2), - ) + val roomMembers = listOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) } val presenter = aRoomDetailsPresenter(room) @@ -134,7 +132,7 @@ class RoomDetailsPresenterTests { // Once updated, the RoomDetailsType becomes 'Dm' val updatedState = awaitItem() - Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(aRoomMember())) + Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember)) cancelAndIgnoreRemainingEvents() } @@ -250,6 +248,7 @@ fun aMatrixRoom( avatarUrl: String? = "https://matrix.org/avatar.jpg", isEncrypted: Boolean = true, isPublic: Boolean = true, + isDirect: Boolean = false, ) = FakeMatrixRoom( roomId = roomId, name = name, @@ -258,6 +257,7 @@ fun aMatrixRoom( avatarUrl = avatarUrl, isEncrypted = isEncrypted, isPublic = isPublic, + isDirect = isDirect, ) fun aRoomMember( From 64c50d44681d613aac7aaf6cdd631e4c7dab1f76 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 Apr 2023 22:13:25 +0200 Subject: [PATCH 20/33] Ignore/Unignore: makes more sense to be at the client level than room --- .../roomdetails/impl/di/RoomMemberModules.kt | 2 +- .../details/RoomMemberDetailsPresenter.kt | 9 +++++---- .../roomdetails/RoomDetailsPresenterTests.kt | 8 +++++++- .../RoomMemberDetailsPresenterTests.kt | 16 ++++++++------- .../libraries/matrix/api/MatrixClient.kt | 2 ++ .../libraries/matrix/api/room/MatrixRoom.kt | 4 ---- .../libraries/matrix/impl/RustMatrixClient.kt | 12 +++++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 20 ++++++------------- .../libraries/matrix/test/FakeMatrixClient.kt | 18 +++++++++++++++++ .../matrix/test/room/FakeMatrixRoom.kt | 4 ---- 10 files changed, 60 insertions(+), 35 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt index b17cb9ab6a..66fae2b50c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt @@ -49,7 +49,7 @@ object RoomMemberProvidesModule { ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(matrixClient.sessionId, room, roomMember) + return RoomMemberDetailsPresenter(matrixClient, room, roomMember) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 8256453a08..33814c29f0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -28,6 +28,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -36,7 +37,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( - private val currentUserSessionId: SessionId, + private val client: MatrixClient, private val room: MatrixRoom, @Assisted private val roomMember: RoomMember, ) : Presenter { @@ -91,16 +92,16 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( avatarUrl = userAvatar, isBlocked = isBlocked.value, displayConfirmationDialog = confirmationDialog, - isCurrentUser = roomMember.userId == currentUserSessionId, + isCurrentUser = roomMember.userId == client.sessionId, eventSink = ::handleEvents ) } private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState) = launch { - room.ignoreUser(userId).onSuccess { isBlockedState.value = true } + client.ignoreUser(userId).onSuccess { isBlockedState.value = true } } private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState) = launch { - room.unignoreUser(userId).onSuccess { isBlockedState.value = false } + client.unignoreUser(userId).onSuccess { isBlockedState.value = false } } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 949cf269d4..2409172f04 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -27,6 +27,7 @@ import io.element.android.features.roomdetails.impl.RoomDetailsType import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -39,6 +40,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -57,7 +59,7 @@ class RoomDetailsPresenterTests { private fun aRoomDetailsPresenter(room: MatrixRoom): RoomDetailsPresenter { val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMember) } } return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory) @@ -240,6 +242,10 @@ class RoomDetailsPresenterTests { } } +fun aMatrixClient( + sessionId: SessionId = A_SESSION_ID, +) = FakeMatrixClient() + fun aMatrixRoom( roomId: RoomId = A_ROOM_ID, name: String? = A_ROOM_NAME, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index a98ec1f971..3731c910fc 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -20,12 +20,12 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import io.element.android.features.roomdetails.aMatrixClient import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState -import io.element.android.libraries.matrix.test.A_SESSION_ID import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -33,6 +33,8 @@ import org.junit.Test @ExperimentalCoroutinesApi class RoomMemberDetailsPresenterTests { + private val matrixClient = aMatrixClient() + @Test fun `present - returns the room member's data, then updates it if needed`() = runTest { val room = aMatrixRoom().apply { @@ -40,7 +42,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success("A custom avatar")) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -63,7 +65,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.failure(Throwable())) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -82,7 +84,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success(null)) } val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -98,7 +100,7 @@ class RoomMemberDetailsPresenterTests { fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -119,7 +121,7 @@ class RoomMemberDetailsPresenterTests { fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -136,7 +138,7 @@ class RoomMemberDetailsPresenterTests { fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(A_SESSION_ID, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index eab4060d85..7bc9ba0797 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -35,6 +35,8 @@ interface MatrixClient : Closeable { val invitesDataSource: RoomSummaryDataSource fun getRoom(roomId: RoomId): MatrixRoom? fun findDM(userId: UserId): MatrixRoom? + suspend fun ignoreUser(userId: UserId): Result + suspend fun unignoreUser(userId: UserId): Result suspend fun createRoom(createRoomParams: CreateRoomParameters): Result suspend fun createDM(userId: UserId): Result fun startSync() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 87afee55da..41cd874098 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -68,10 +68,6 @@ interface MatrixRoom : Closeable { suspend fun redactEvent(eventId: EventId, reason: String? = null): Result - suspend fun ignoreUser(userId: UserId): Result - - suspend fun unignoreUser(userId: UserId): Result - suspend fun leave(): Result suspend fun acceptInvitation(): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 2a7e7c0826..b710a5cf6d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -212,6 +212,18 @@ class RustMatrixClient constructor( return roomId?.let { getRoom(it) } } + override suspend fun ignoreUser(userId: UserId): Result = withContext(dispatchers.io) { + runCatching { + client.ignoreUser(userId.value) + } + } + + override suspend fun unignoreUser(userId: UserId): Result = withContext(dispatchers.io) { + runCatching { + client.unignoreUser(userId.value) + } + } + override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = withContext(dispatchers.io) { runCatching { val rustParams = RustCreateRoomParameters( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 95094cfdc6..1e75867215 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember class RustMatrixRoom( @@ -200,19 +201,10 @@ class RustMatrixRoom( } } - override suspend fun ignoreUser(userId: UserId): Result { - return runCatching { - getRustMember(userId)?.ignore() ?: error("No member with userId $userId exists in room $roomId") - } - } - - override suspend fun unignoreUser(userId: UserId): Result { - return runCatching { - getRustMember(userId)?.unignore() ?: error("No member with userId $userId exists in room $roomId") - } - } - - private fun getRustMember(userId: UserId): RustRoomMember? { - return innerRoom.members().find { it.userId() == userId.value } + private fun findRoomMember(userId: UserId, action: (RustRoomMember).() -> Unit) { + return innerRoom.members() + .find { it.userId() == userId.value } + ?.use(action) + ?: error("No member with userId $userId exists in room $roomId") } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index d33bd5c939..c547ac8237 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -47,6 +47,8 @@ class FakeMatrixClient( private val notificationService: FakeNotificationService = FakeNotificationService(), ) : MatrixClient { + private var ignoreUserResult: Result = Result.success(Unit) + private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) private var createDmResult: Result = Result.success(A_ROOM_ID) private var createDmFailure: Throwable? = null @@ -62,6 +64,14 @@ class FakeMatrixClient( return findDmResult } + override suspend fun ignoreUser(userId: UserId): Result { + return ignoreUserResult + } + + override suspend fun unignoreUser(userId: UserId): Result { + return unignoreUserResult + } + override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result { delay(100) return createRoomResult @@ -130,6 +140,14 @@ class FakeMatrixClient( createDmResult = result } + fun givenIgnoreUserResult(result: Result) { + ignoreUserResult = result + } + + fun givenUnignoreUserResult(result: Result) { + unignoreUserResult = result + } + fun givenCreateDmError(failure: Throwable?) { createDmFailure = failure } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index c0f4a7ee20..28d6525739 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -117,10 +117,6 @@ class FakeMatrixRoom( return Result.success(Unit) } - override suspend fun ignoreUser(userId: UserId): Result = ignoreResult - - override suspend fun unignoreUser(userId: UserId): Result = unignoreResult - override suspend fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) override suspend fun acceptInvitation(): Result { isInviteAccepted = true From 4b9f2fdae0a8a877d583b9b14fc6a5b66f5108ba Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 Apr 2023 22:38:52 +0200 Subject: [PATCH 21/33] Don't pass RoomMember to Node but a UserId instead --- .../roomdetails/impl/RoomDetailsFlowNode.kt | 8 ++-- .../roomdetails/impl/RoomDetailsPresenter.kt | 2 +- .../roomdetails/impl/di/RoomMemberModules.kt | 5 ++- .../impl/members/RoomMemberListNode.kt | 7 +-- .../impl/members/RoomMemberListPresenter.kt | 16 +------ .../impl/members/RoomMemberListState.kt | 2 - .../members/RoomMemberListStateProvider.kt | 1 - .../impl/members/RoomMemberListView.kt | 13 ++---- .../members/details/RoomMemberDetailsNode.kt | 8 ++-- .../details/RoomMemberDetailsPresenter.kt | 44 ++++++++++++------- .../roomdetails/RoomDetailsPresenterTests.kt | 4 +- .../libraries/matrix/api/room/RoomMember.kt | 5 +-- 12 files changed, 52 insertions(+), 63 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index f86b9952a8..f3a0ef9001 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -59,7 +59,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( object RoomMemberList : NavTarget @Parcelize - data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget + data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -74,14 +74,14 @@ class RoomDetailsFlowNode @AssistedInject constructor( } NavTarget.RoomMemberList -> { val roomMemberListCallback = object : RoomMemberListNode.Callback { - override fun openRoomMemberDetails(roomMember: RoomMember) { - backstack.push(NavTarget.RoomMemberDetails(roomMember)) + override fun openRoomMemberDetails(roomMemberId: UserId) { + backstack.push(NavTarget.RoomMemberDetails(roomMemberId)) } } createNode(buildContext, listOf(roomMemberListCallback)) } is NavTarget.RoomMemberDetails -> { - createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember))) + createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId))) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index bb4cae8423..3479adbb81 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -99,7 +99,7 @@ class RoomDetailsPresenter @Inject constructor( @Composable private fun roomMemberDetailsPresenter(dmMemberState: RoomMember?) = remember(dmMemberState) { dmMemberState?.let { roomMember -> - roomMembersDetailsPresenterFactory.create(roomMember) + roomMembersDetailsPresenterFactory.create(roomMember.userId) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt index 66fae2b50c..2cd3b7eb08 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModules.kt @@ -25,6 +25,7 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import javax.inject.Named @@ -48,8 +49,8 @@ object RoomMemberProvidesModule { room: MatrixRoom, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { - override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(matrixClient, room, roomMember) + override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { + return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 48743b66fa..5b1e7a72e0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -26,6 +26,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember @ContributesNode(RoomScope::class) @@ -36,14 +37,14 @@ class RoomMemberListNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun openRoomMemberDetails(roomMember: RoomMember) + fun openRoomMemberDetails(roomMemberId: UserId) } private val callbacks = plugins() - private fun openRoomMemberDetails(roomMember: RoomMember) { + private fun openRoomMemberDetails(roomMemberId: UserId) { callbacks.forEach { - it.openRoomMemberDetails(roomMember) + it.openRoomMemberDetails(roomMemberId) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 897e56728b..4d87c0e158 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListDataStore @@ -61,33 +60,20 @@ class RoomMemberListPresenter @Inject constructor( @Composable override fun present(): RoomMemberListState { - val coroutineScope = rememberCoroutineScope() val userListState = userListPresenter.present() val allUsers = remember { mutableStateOf>>(Async.Loading()) } - val selectedMember: MutableState = remember { - mutableStateOf(null) - } + LaunchedEffect(Unit) { withContext(coroutineDispatchers.io) { allUsers.value = Async.Success(userListDataSource.search("").toImmutableList()) } } - fun handleEvents(roomMemberListEvents: RoomMemberListEvents) { - when (roomMemberListEvents) { - is RoomMemberListEvents.SelectUser -> coroutineScope.loadRoomMember(roomMemberListEvents.user, selectedMember) - } - } return RoomMemberListState( allUsers = allUsers.value, userListState = userListState, - selectedRoomMember = selectedMember.value, - eventSink = ::handleEvents ) } - private fun CoroutineScope.loadRoomMember(user: MatrixUser, selectedMember: MutableState) = launch(coroutineDispatchers.io) { - selectedMember.value = room.getMemberFlow(user.id).firstOrNull() - } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt index 42289b9ebe..28885006b1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -25,6 +25,4 @@ import kotlinx.collections.immutable.ImmutableList data class RoomMemberListState( val allUsers: Async>, val userListState: UserListState, - val selectedRoomMember: RoomMember? = null, - val eventSink: (RoomMemberListEvents) -> Unit, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 57012c6d86..fc98ae7544 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -39,5 +39,4 @@ internal fun aRoomMemberListState( RoomMemberListState( userListState = aUserListState().copy(searchResults = searchResults), allUsers = allUsers, - eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 47aeb635df..99c6a9299a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource @@ -52,7 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser @OptIn(ExperimentalMaterial3Api::class) @@ -61,17 +60,11 @@ fun RoomMemberListView( state: RoomMemberListState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onMemberSelected: (RoomMember) -> Unit = {}, + onMemberSelected: (UserId) -> Unit = {}, ) { - LaunchedEffect(state.selectedRoomMember) { - if (state.selectedRoomMember != null) { - onMemberSelected(state.selectedRoomMember) - } - } - fun onUserSelected(user: MatrixUser) { - state.eventSink(RoomMemberListEvents.SelectUser(user)) + onMemberSelected(user.id) } Scaffold( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 72e335c1d2..7fd4dd3876 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -30,8 +30,8 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder -import io.element.android.libraries.matrix.api.room.RoomMember import timber.log.Timber import io.element.android.libraries.androidutils.R as AndroidUtilsR @@ -43,18 +43,18 @@ class RoomMemberDetailsNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { data class Inputs( - val member: RoomMember, + val roomMemberId: UserId, ) : NodeInputs private val inputs = inputs() - private val presenter = presenterFactory.create(inputs.member) + private val presenter = presenterFactory.create(inputs.roomMemberId) @Composable override fun View(modifier: Modifier) { val context = LocalContext.current fun onShareUser() { - val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.member.userId) + val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.roomMemberId) permalinkResult.onSuccess { permalink -> startSharePlainTextIntent( context = context, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 33814c29f0..e51205728e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -28,29 +29,33 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.getMemberFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( private val client: MatrixClient, private val room: MatrixRoom, - @Assisted private val roomMember: RoomMember, + @Assisted private val roomMemberId: UserId, ) : Presenter { interface Factory { - fun create(roomMember: RoomMember): RoomMemberDetailsPresenter + fun create(roomMemberId: UserId): RoomMemberDetailsPresenter } @Composable override fun present(): RoomMemberDetailsState { val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } - val isBlocked = remember { mutableStateOf(roomMember.isIgnored) } + val roomMember by room.getMemberFlow(roomMemberId).collectAsState(initial = null) + + val isBlocked = remember(roomMember?.isIgnored) { + mutableStateOf(roomMember?.isIgnored.orFalse()) + } fun handleEvents(event: RoomMemberDetailsEvents) { when (event) { @@ -59,7 +64,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( confirmationDialog = ConfirmationDialog.Block } else { confirmationDialog = null - coroutineScope.blockUser(roomMember.userId, isBlocked) + coroutineScope.blockUser(roomMemberId, isBlocked) } } is RoomMemberDetailsEvents.UnblockUser -> { @@ -67,41 +72,50 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( confirmationDialog = ConfirmationDialog.Unblock } else { confirmationDialog = null - coroutineScope.unblockUser(roomMember.userId, isBlocked) + coroutineScope.unblockUser(roomMemberId, isBlocked) } } RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null } } - val userName by produceState(initialValue = roomMember.displayName) { - room.userDisplayName(roomMember.userId).onSuccess { displayName -> + val userName by produceState(initialValue = roomMember?.displayName) { + room.userDisplayName(roomMemberId).onSuccess { displayName -> if (displayName != null) value = displayName } } - val userAvatar by produceState(initialValue = roomMember.avatarUrl) { - room.userAvatarUrl(roomMember.userId).onSuccess { avatarUrl -> + val userAvatar by produceState(initialValue = roomMember?.avatarUrl) { + room.userAvatarUrl(roomMemberId).onSuccess { avatarUrl -> if (avatarUrl != null) value = avatarUrl } } return RoomMemberDetailsState( - userId = roomMember.userId.value, + userId = roomMemberId.value, userName = userName, avatarUrl = userAvatar, isBlocked = isBlocked.value, displayConfirmationDialog = confirmationDialog, - isCurrentUser = roomMember.userId == client.sessionId, + isCurrentUser = roomMember?.userId == client.sessionId, eventSink = ::handleEvents ) } private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState) = launch { - client.ignoreUser(userId).onSuccess { isBlockedState.value = true } + client.ignoreUser(userId) + .map { + isBlockedState.value = true + room.updateMembers() + } + } private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState) = launch { - client.unignoreUser(userId).onSuccess { isBlockedState.value = false } + client.unignoreUser(userId) + .map { + isBlockedState.value = false + room.updateMembers() + } } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 2409172f04..bd0b5906cb 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -58,8 +58,8 @@ class RoomDetailsPresenterTests { private fun aRoomDetailsPresenter(room: MatrixRoom): RoomDetailsPresenter { val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { - override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMember) + override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { + return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMemberId) } } return RoomDetailsPresenter(room, roomMembershipObserver, testCoroutineDispatchers, roomMemberDetailsPresenterFactory) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 25bb3e8a7e..3c9bd030b0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -16,11 +16,8 @@ package io.element.android.libraries.matrix.api.room -import android.os.Parcelable import io.element.android.libraries.matrix.api.core.UserId -import kotlinx.parcelize.Parcelize -@Parcelize data class RoomMember( val userId: UserId, val displayName: String?, @@ -30,7 +27,7 @@ data class RoomMember( val powerLevel: Long, val normalizedPowerLevel: Long, val isIgnored: Boolean, -) : Parcelable +) enum class RoomMembershipState { BAN, INVITE, JOIN, KNOCK, LEAVE From eeca1c9ee32a9c526dae7f1914f359ad2b7a7e54 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 28 Apr 2023 10:52:34 +0200 Subject: [PATCH 22/33] Add `PickerLauncher` wrapper for media/file pickers. (#361) * Add `PickerLauncher` wrapper for media/file pickers. * Add FileProvider path, handle Camera picker and add NoOp implementation to fix tests. * Move media pickers to their own module. * Add missing media pickers * Add feature flag and some extra tests --- app/src/main/AndroidManifest.xml | 10 ++ app/src/main/res/xml/file_providers.xml | 19 +++ features/messages/impl/build.gradle.kts | 3 + .../messages/ExampleInstrumentedTest.kt | 38 ----- .../textcomposer/MessageComposerEvents.kt | 2 + .../textcomposer/MessageComposerPresenter.kt | 24 ++- .../impl/textcomposer/MessageComposerView.kt | 3 + .../messages/MessagesPresenterTest.kt | 6 +- .../MessageComposerPresenterTest.kt | 67 ++++++-- libraries/mediapickers/build.gradle.kts | 34 ++++ .../libraries/mediapickers/PickerLauncher.kt | 51 ++++++ .../libraries/mediapickers/PickerProvider.kt | 150 ++++++++++++++++++ .../libraries/mediapickers/PickerType.kt | 58 +++++++ .../libraries/mediapickers/PickerTypeTests.kt | 65 ++++++++ .../libraries/textcomposer/TextComposer.kt | 2 + .../kotlin/extension/DependencyHandleScope.kt | 2 +- 16 files changed, 483 insertions(+), 51 deletions(-) create mode 100644 app/src/main/res/xml/file_providers.xml delete mode 100644 features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt create mode 100644 libraries/mediapickers/build.gradle.kts create mode 100644 libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt create mode 100644 libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt create mode 100644 libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt create mode 100644 libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 342e05532c..4a81d52d46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,16 @@ android:supportsRtl="true" android:theme="@style/Theme.ElementX" tools:targetApi="33"> + + + + + + + + + diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 9b43d41d4d..1eb3840eb3 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { implementation(projects.libraries.textcomposer) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.mediapickers) + implementation(projects.libraries.featureflag.api) implementation(projects.features.networkmonitor.api) implementation(libs.coil.compose) implementation(libs.datetime) @@ -55,6 +57,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.libraries.featureflag.test) androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) diff --git a/features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt b/features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt deleted file mode 100644 index 97ef4f5a3b..0000000000 --- a/features/messages/impl/src/androidTest/kotlin/io/element/android/features/messages/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,38 +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.features.messages - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("io.element.android.features.messages.test", appContext.packageName) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt index 1da1188ee4..610f6e1ec0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt @@ -24,4 +24,6 @@ sealed interface MessageComposerEvents { object CloseSpecialMode : MessageComposerEvents data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents data class UpdateText(val text: CharSequence) : MessageComposerEvents + + object TakePhoto : MessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt index a5cc266e02..603776e518 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt @@ -21,23 +21,37 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.toStableCharSequence +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.mediapickers.PickerProvider import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject class MessageComposerPresenter @Inject constructor( private val appCoroutineScope: CoroutineScope, - private val room: MatrixRoom + private val room: MatrixRoom, + private val mediaPickerProvider: PickerProvider, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): MessageComposerState { + val localCoroutineScope = rememberCoroutineScope() + + // Example usage of custom pickers + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri -> + Timber.d("Photo saved at $uri") + }) + val isFullScreen = rememberSaveable { mutableStateOf(false) } @@ -63,9 +77,14 @@ class MessageComposerPresenter @Inject constructor( text.value = "".toStableCharSequence() composerMode.setToNormal() } + is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode - } + MessageComposerEvents.TakePhoto -> localCoroutineScope.launch { + if (featureFlagService.isFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow)) { + cameraPhotoPicker.launch() + } + }} } return MessageComposerState( @@ -92,6 +111,7 @@ class MessageComposerPresenter @Inject constructor( capturedMode.eventId, text ) + is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Reply -> room.replyMessage( capturedMode.eventId, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt index e94ebb0a2a..ebdfca6f9e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt @@ -53,6 +53,9 @@ fun MessageComposerView( composerMode = state.mode, onCloseSpecialMode = ::onCloseSpecialMode, onComposerTextChange = ::onComposerTextChange, + onAddAttachment = { + state.eventSink(MessageComposerEvents.TakePhoto) + }, composerCanSendMessage = state.isSendButtonVisible, composerText = state.text?.charSequence?.toString(), isInDarkMode = !ElementTheme.colors.isLight, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index a7bc112174..83abbb2e0e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -31,10 +31,12 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.mediapickers.PickerProvider import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -129,7 +131,9 @@ class MessagesPresenterTest { ): MessagesPresenter { val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, - room = matrixRoom + room = matrixRoom, + mediaPickerProvider = PickerProvider(isInTest = true), + featureFlagService = FakeFeatureFlagService(), ) val timelinePresenter = TimelinePresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 105d2ce31a..b18e9631ef 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -27,23 +27,37 @@ import io.element.android.features.messages.impl.textcomposer.MessageComposerEve import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter import io.element.android.features.messages.impl.textcomposer.MessageComposerState import io.element.android.libraries.core.data.StableCharSequence +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_REPLY import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.mediapickers.PickerProvider import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Test class MessageComposerPresenterTest { + + private val pickerProvider = PickerProvider(isInTest = true) + private val featureFlagService = FakeFeatureFlagService().apply { + runBlocking { + setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true) + } + } + @Test fun `present - initial state`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -60,7 +74,9 @@ class MessageComposerPresenterTest { fun `present - toggle fullscreen`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -79,7 +95,9 @@ class MessageComposerPresenterTest { fun `present - change message`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -100,7 +118,9 @@ class MessageComposerPresenterTest { fun `present - change mode to edit`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -130,7 +150,9 @@ class MessageComposerPresenterTest { fun `present - change mode to reply`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -150,7 +172,9 @@ class MessageComposerPresenterTest { fun `present - change mode to quote`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -170,7 +194,9 @@ class MessageComposerPresenterTest { fun `present - send message`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom() + FakeMatrixRoom(), + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -192,7 +218,9 @@ class MessageComposerPresenterTest { val fakeMatrixRoom = FakeMatrixRoom() val presenter = MessageComposerPresenter( this, - fakeMatrixRoom + fakeMatrixRoom, + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -223,7 +251,9 @@ class MessageComposerPresenterTest { val fakeMatrixRoom = FakeMatrixRoom() val presenter = MessageComposerPresenter( this, - fakeMatrixRoom + fakeMatrixRoom, + pickerProvider, + featureFlagService, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -248,6 +278,25 @@ class MessageComposerPresenterTest { assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY) } } + + @Test + fun `present - Take photo`() = runTest { + val fakeMatrixRoom = FakeMatrixRoom() + val presenter = MessageComposerPresenter( + this, + fakeMatrixRoom, + pickerProvider, + featureFlagService, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(MessageComposerEvents.TakePhoto) + + // TODO verify some post processing of the captured image is done + } + } } fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE) diff --git a/libraries/mediapickers/build.gradle.kts b/libraries/mediapickers/build.gradle.kts new file mode 100644 index 0000000000..444244d2f0 --- /dev/null +++ b/libraries/mediapickers/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.mediapickers" + + dependencies { + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.core) + implementation(libs.inject) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) + } +} diff --git a/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt new file mode 100644 index 0000000000..16f21a9683 --- /dev/null +++ b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerLauncher.kt @@ -0,0 +1,51 @@ +/* + * 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.mediapickers + +import androidx.activity.compose.ManagedActivityResultLauncher + +/** + * Wrapper around [ManagedActivityResultLauncher] to be used with media/file pickers. + */ +interface PickerLauncher { + /** Starts the activity result launcher with its default input. */ + fun launch() + + /** Starts the activity result launcher with a [customInput]. */ + fun launch(customInput: Input) +} + +class ComposePickerLauncher( + private val managedLauncher: ManagedActivityResultLauncher, + private val defaultRequest: Input, +) : PickerLauncher { + override fun launch() { + managedLauncher.launch(defaultRequest) + } + + override fun launch(customInput: Input) { + managedLauncher.launch(customInput) + } +} + +/** Needed for screenshot tests. */ +class NoOpPickerLauncher( + private val onResult: () -> Unit, +) : PickerLauncher { + override fun launch() = onResult() + override fun launch(customInput: Input) = onResult() +} diff --git a/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt new file mode 100644 index 0000000000..720378aaca --- /dev/null +++ b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerProvider.kt @@ -0,0 +1,150 @@ +/* + * 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.mediapickers + +import android.content.Context +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.core.content.FileProvider +import io.element.android.libraries.core.mimetype.MimeTypes +import java.io.File +import java.util.UUID +import javax.inject.Inject + +class PickerProvider constructor(private val isInTest: Boolean) { + + @Inject + constructor(): this(false) + + /** + * Remembers and returns a [PickerLauncher] for a certain media/file [type]. + */ + @Composable + internal fun rememberPickerLauncher( + type: PickerType, + onResult: (Output) -> Unit, + ): PickerLauncher { + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { } + } else { + val contract = type.getContract() + val managedLauncher = rememberLauncherForActivityResult(contract = contract, onResult = onResult) + remember(type) { ComposePickerLauncher(managedLauncher, type.getDefaultRequest()) } + } + } + + /** + * Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video. + * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. + */ + @Composable + fun registerGalleryPicker(onResult: (Uri?) -> Unit): PickerLauncher { + // Tests and UI preview can't handle Contexts, so we might as well disable the whole picker + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { onResult(null) } + } else { + rememberPickerLauncher(type = PickerType.ImageAndVideo) { uri -> onResult(uri) } + } + } + + /** + * Remembers and returns a [PickerLauncher] for a file of a certain [mimeType] (any type of file, by default). + * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. + */ + @Composable + fun registerFilePicker(mimeType: String = MimeTypes.Any, onResult: (Uri?) -> Unit): PickerLauncher { + // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { onResult(null) } + } else { + rememberPickerLauncher(type = PickerType.File(mimeType)) { uri -> onResult(uri) } + } + } + + /** + * Remembers and returns a [PickerLauncher] for taking a photo with a camera app. + * @param [onResult] will be called with either the photo's [Uri] or `null` if nothing was selected. + * @param [deleteAfter] When it's `true`, the taken photo will be automatically removed after calling [onResult]. + * It's `true` by default. + */ + @Composable + fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher { + // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { onResult(null) } + } else { + val context = LocalContext.current + val tmpFile = remember { getTemporaryFile(context) } + val tmpFileUri = remember(tmpFile) { getTemporaryUri(context, tmpFile) } + rememberPickerLauncher(type = PickerType.Camera.Photo(tmpFileUri)) { success -> + // Execute callback + onResult(if (success) tmpFileUri else null) + // Then remove the file and clear the picker + if (deleteAfter) { + tmpFile.delete() + } + } + } + } + + /** + * Remembers and returns a [PickerLauncher] for recording a video with a camera app. + * @param [onResult] will be called with either the video's [Uri] or `null` if nothing was selected. + * @param [deleteAfter] When it's `true`, the recorded video will be automatically removed after calling [onResult]. + * It's `true` by default. + */ + @Composable + fun registerCameraVideoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher { + // Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { onResult(null) } + } else { + val context = LocalContext.current + val tmpFile = remember { getTemporaryFile(context) } + val tmpFileUri = remember(tmpFile) { getTemporaryUri(context, tmpFile) } + rememberPickerLauncher(type = PickerType.Camera.Video(tmpFileUri)) { success -> + // Execute callback + onResult(if (success) tmpFileUri else null) + // Then remove the file and clear the picker + if (deleteAfter) { + tmpFile.delete() + } + } + } + } + + private fun getTemporaryFile( + context: Context, + baseFolder: File = context.cacheDir, + filename: String = UUID.randomUUID().toString(), + ): File { + return File(baseFolder, filename) + } + + private fun getTemporaryUri( + context: Context, + file: File, + ): Uri { + val authority = "${context.packageName}.fileprovider" + return FileProvider.getUriForFile(context, authority, file) + } +} diff --git a/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt new file mode 100644 index 0000000000..354d9a4918 --- /dev/null +++ b/libraries/mediapickers/src/main/kotlin/io/element/android/libraries/mediapickers/PickerType.kt @@ -0,0 +1,58 @@ +/* + * 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.mediapickers + +import android.net.Uri +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import io.element.android.libraries.core.mimetype.MimeTypes + +sealed interface PickerType { + fun getContract(): ActivityResultContract + fun getDefaultRequest(): Input + + object ImageAndVideo : PickerType { + override fun getContract() = ActivityResultContracts.PickVisualMedia() + override fun getDefaultRequest(): PickVisualMediaRequest { + return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) + } + } + + object Camera { + data class Photo(val destUri: Uri) : PickerType { + override fun getContract() = ActivityResultContracts.TakePicture() + override fun getDefaultRequest(): Uri { + return destUri + } + } + + data class Video(val destUri: Uri) : PickerType { + override fun getContract() = ActivityResultContracts.CaptureVideo() + override fun getDefaultRequest(): Uri { + return destUri + } + } + } + + data class File(val mimeType: String = MimeTypes.Any) : PickerType { + override fun getContract() = ActivityResultContracts.GetContent() + override fun getDefaultRequest(): String { + return mimeType + } + } +} diff --git a/libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt b/libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt new file mode 100644 index 0000000000..693ef40cfe --- /dev/null +++ b/libraries/mediapickers/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt @@ -0,0 +1,65 @@ +/* + * 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.mediapickers + +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContracts +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PickerTypeTests { + + @Test + fun `ImageAndVideo - assert types`() { + val pickerType = PickerType.ImageAndVideo + assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.PickVisualMedia::class.java) + assertThat(pickerType.getDefaultRequest().mediaType).isEqualTo(ActivityResultContracts.PickVisualMedia.ImageAndVideo) + } + + @Test + fun `File - assert types`() { + val pickerType = PickerType.File() + assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.GetContent::class.java) + assertThat(pickerType.getDefaultRequest()).isEqualTo(MimeTypes.Any) + + val mimeType = MimeTypes.Images + val customPickerType = PickerType.File(mimeType) + assertThat(customPickerType.getContract()).isInstanceOf(ActivityResultContracts.GetContent::class.java) + assertThat(customPickerType.getDefaultRequest()).isEqualTo(mimeType) + } + + @Test + fun `CameraPhoto - assert types`() { + val uri = Uri.parse("file:///tmp/test") + val pickerType = PickerType.Camera.Photo(uri) + assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.TakePicture::class.java) + assertThat(pickerType.getDefaultRequest()).isEqualTo(uri) + } + + @Test + fun `CameraVideo - assert types`() { + val uri = Uri.parse("file:///tmp/test") + val pickerType = PickerType.Camera.Video(uri) + assertThat(pickerType.getContract()).isInstanceOf(ActivityResultContracts.CaptureVideo::class.java) + assertThat(pickerType.getDefaultRequest()).isEqualTo(uri) + } + +} diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 3481a56388..bab573de58 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -50,6 +50,7 @@ fun TextComposer( onFullscreenToggle: () -> Unit = {}, onCloseSpecialMode: () -> Unit = {}, onComposerTextChange: (CharSequence) -> Unit = {}, + onAddAttachment:() -> Unit = {}, ) { if (LocalInspectionMode.current) { FakeComposer(modifier) @@ -78,6 +79,7 @@ fun TextComposer( } override fun onAddAttachment() { + onAddAttachment() } override fun onExpandOrCompactChange() { diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 5d1ef9a20e..18ce6e55f8 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -90,7 +90,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:di")) implementation(project(":libraries:session-storage:impl")) implementation(project(":libraries:statemachine")) - + implementation(project(":libraries:mediapickers")) } fun DependencyHandlerScope.allServicesImpl() { From 82b50d9e4adb3cc01b9d4366d29e73d8b9e7bdc0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:05:14 +0000 Subject: [PATCH 23/33] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.11 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 275145a865..18d214f4f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -128,7 +128,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.10" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.11" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } From 23a7b871f73dc919f289b18a3e9bfedfe20cd3f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 Apr 2023 15:04:33 +0200 Subject: [PATCH 24/33] RoomMembers: change the API again.. --- .../roomdetails/impl/RoomDetailsPresenter.kt | 19 +++--- .../roomdetails/impl/RoomDetailsView.kt | 2 +- .../impl/members/RoomMemberListPresenter.kt | 7 --- .../impl/members/RoomUserListDataSource.kt | 1 + .../details/RoomMemberDetailsPresenter.kt | 14 +++-- .../roomdetails/RoomDetailsPresenterTests.kt | 8 +-- .../RoomMemberDetailsPresenterTests.kt | 22 ++++--- .../libraries/matrix/api/room/MatrixRoom.kt | 20 ------- .../matrix/api/room/MatrixRoomMembersState.kt | 12 ++-- .../matrix/impl/room/RustMatrixRoom.kt | 16 ++--- .../impl/timeline/RustMatrixTimeline.kt | 3 +- .../matrix/ui/room/MatrixRoomMembers.kt | 58 +++++++++++++++++++ 12 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 3479adbb81..aa46dacd6d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState @@ -33,7 +34,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.room.getDmMemberFlow +import io.element.android.libraries.matrix.ui.room.directRoomMember import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -54,14 +55,14 @@ class RoomDetailsPresenter @Inject constructor( val error = remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + room.updateMembers() + } val membersState by room.membersStateFlow.collectAsState() val memberCount by getMemberCount(membersState) - val dmMemberState by room.getDmMemberFlow() - .collectAsState(initial = null, context = coroutineDispatchers.computation) - - val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMemberState) - - val roomType = getRoomType(dmMemberState) + val dmMember by room.directRoomMember() + val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) + val roomType = getRoomType(dmMember) fun handleEvents(event: RoomDetailsEvent) { when (event) { @@ -119,9 +120,9 @@ class RoomDetailsPresenter @Inject constructor( derivedStateOf { when (membersState) { MatrixRoomMembersState.Unknown -> Async.Uninitialized - MatrixRoomMembersState.Pending -> Async.Loading() + is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size) is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size) - is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 9b3f83d456..dbee610dba 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -117,7 +117,7 @@ fun RoomDetailsView( } if (state.roomType is RoomDetailsType.Room) { - val memberCount = (state.memberCount as? Async.Success)?.state + val memberCount = state.memberCount.dataOrNull() MembersSection( memberCount = memberCount, isLoading = state.memberCount.isLoading(), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 4d87c0e158..8d66d60bc2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -18,7 +18,6 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.features.userlist.api.SelectionMode @@ -30,14 +29,9 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.getMemberFlow import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Named @@ -74,6 +68,5 @@ class RoomMemberListPresenter @Inject constructor( userListState = userListState, ) } - } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index 9133db7688..e0559f3caf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -41,6 +41,7 @@ class RoomUserListDataSource @Inject constructor( .dropWhile { it !is MatrixRoomMembersState.Ready } .first() .roomMembers() + .orEmpty() val filteredMembers = if (query.isBlank()) { roomMembers } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index e51205728e..904d4f183b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -17,8 +17,8 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -33,9 +33,10 @@ import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.getMemberFlow +import io.element.android.libraries.matrix.ui.room.roomMember import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber class RoomMemberDetailsPresenter @AssistedInject constructor( private val client: MatrixClient, @@ -51,11 +52,14 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( override fun present(): RoomMemberDetailsState { val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } - val roomMember by room.getMemberFlow(roomMemberId).collectAsState(initial = null) - - val isBlocked = remember(roomMember?.isIgnored) { + val roomMember by room.roomMember(roomMemberId) + // the room member is not really live... + val isBlocked = remember { mutableStateOf(roomMember?.isIgnored.orFalse()) } + LaunchedEffect(Unit) { + room.updateMembers() + } fun handleEvents(event: RoomMemberDetailsEvents) { when (event) { diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index bd0b5906cb..585a61f05d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -96,6 +96,7 @@ class RoomDetailsPresenterTests { room.givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) val finalState = awaitItem() Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) + cancelAndIgnoreRemainingEvents() } } @@ -129,12 +130,7 @@ class RoomDetailsPresenterTests { presenter.present() }.test { val initialState = awaitItem() - // It's not configured yet in the first iteration - Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Room) - - // Once updated, the RoomDetailsType becomes 'Dm' - val updatedState = awaitItem() - Truth.assertThat(updatedState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember)) + Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember)) cancelAndIgnoreRemainingEvents() } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index 3731c910fc..13eb28ca85 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -26,6 +26,7 @@ import io.element.android.features.roomdetails.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,12 +38,13 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - returns the room member's data, then updates it if needed`() = runTest { + val roomMember = aRoomMember(displayName = "Alice") val room = aMatrixRoom().apply { givenUserDisplayNameResult(Result.success("A custom name")) givenUserAvatarUrlResult(Result.success("A custom avatar")) + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) } - val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -60,12 +62,13 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - will recover when retrieving room member details fails`() = runTest { + val roomMember = aRoomMember(displayName = "Alice") val room = aMatrixRoom().apply { givenUserDisplayNameResult(Result.failure(Throwable())) givenUserAvatarUrlResult(Result.failure(Throwable())) + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) } - val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) + val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -79,12 +82,13 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - will fallback to original data if the updated data is null`() = runTest { + val roomMember = aRoomMember(displayName = "Alice") val room = aMatrixRoom().apply { givenUserDisplayNameResult(Result.success(null)) givenUserAvatarUrlResult(Result.success(null)) + givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) } - val roomMember = aRoomMember(displayName = "Alice") - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) + val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -100,7 +104,7 @@ class RoomMemberDetailsPresenterTests { fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) + val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -121,7 +125,7 @@ class RoomMemberDetailsPresenterTests { fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) + val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -138,7 +142,7 @@ class RoomMemberDetailsPresenterTests { fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember) + val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 41cd874098..35451874aa 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map import java.io.Closeable interface MatrixRoom : Closeable { @@ -74,22 +73,3 @@ interface MatrixRoom : Closeable { suspend fun rejectInvitation(): Result } - -fun MatrixRoom.getMemberFlow(userId: UserId): Flow { - return membersStateFlow.map { state -> - state.roomMembers().find { - it.userId == userId - } - } -} - -fun MatrixRoom.getDmMemberFlow(): Flow { - return membersStateFlow.map { state -> - val members = state.roomMembers() - if (members.size == 2 && isDirect && isEncrypted) { - members.find { it.userId != this.sessionId } - } else { - null - } - } -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt index 319a5f7605..4e41fd43ba 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -18,14 +18,18 @@ package io.element.android.libraries.matrix.api.room sealed interface MatrixRoomMembersState { object Unknown : MatrixRoomMembersState - object Pending : MatrixRoomMembersState - data class Error(val failure: Throwable) : MatrixRoomMembersState + data class Pending(val prevRoomMembers: List? = null) : MatrixRoomMembersState + data class Error(val failure: Throwable, val prevRoomMembers: List? = null) : MatrixRoomMembersState data class Ready(val roomMembers: List) : MatrixRoomMembersState } -fun MatrixRoomMembersState.roomMembers(): List { +fun MatrixRoomMembersState.roomMembers(): List? { return when (this) { is MatrixRoomMembersState.Ready -> roomMembers - else -> emptyList() + is MatrixRoomMembersState.Pending -> prevRoomMembers + is MatrixRoomMembersState.Error -> prevRoomMembers + else -> null } } + + diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 1e75867215..9caba227ce 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import kotlinx.coroutines.CoroutineScope @@ -32,14 +33,13 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown -import org.matrix.rustcomponents.sdk.use -import org.matrix.rustcomponents.sdk.RoomMember as RustRoomMember class RustMatrixRoom( override val sessionId: SessionId, @@ -128,13 +128,15 @@ class RustMatrixRoom( get() = innerRoom.isDirect() override suspend fun updateMembers(): Result = withContext(coroutineDispatchers.io) { - _membersStateFlow.value = MatrixRoomMembersState.Pending + val currentState = _membersStateFlow.value + val currentMembers = currentState.roomMembers() + _membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers) runCatching { innerRoom.members().map(RoomMemberMapper::map) }.map { _membersStateFlow.value = MatrixRoomMembersState.Ready(it) }.onFailure { - _membersStateFlow.value = MatrixRoomMembersState.Error(it) + _membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = it) } } @@ -201,10 +203,4 @@ class RustMatrixRoom( } } - private fun findRoomMember(userId: UserId, action: (RustRoomMember).() -> Unit) { - return innerRoom.members() - .find { it.userId() == userId.value } - ?.use(action) - ?: error("No member with userId $userId exists in room $roomId") - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index bffaa30886..6f64db76ef 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -153,8 +153,7 @@ class RustMatrixTimeline( RequiredState(key = "m.room.topic", value = ""), RequiredState(key = "m.room.join_rules", value = ""), ), - //TODO allow configuration - timelineLimit = 20.toUInt() + timelineLimit = null ) val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, settings) launch { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt new file mode 100644 index 0000000000..2df7c1a152 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -0,0 +1,58 @@ +/* + * 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.ui.room + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.roomMembers + +@Composable +fun MatrixRoom.roomMember(userId: UserId): State { + val roomMembersState by membersStateFlow.collectAsState() + val roomMembers = roomMembersState.roomMembers() + return remember(roomMembers) { + derivedStateOf { + roomMembers?.find { + it.userId == userId + } + } + } +} + +@Composable +fun MatrixRoom.directRoomMember(): State { + val roomMembersState by membersStateFlow.collectAsState() + val roomMembers = roomMembersState.roomMembers() + return remember(roomMembers) { + derivedStateOf { + if (roomMembers == null) { + null + } else if (roomMembers.size == 2 && isDirect && isEncrypted) { + roomMembers.find { it.userId != this.sessionId } + } else { + null + } + } + } +} From cd9320e06abf395445dfed710520d43d084e7944 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Apr 2023 14:54:19 +0000 Subject: [PATCH 25/33] Update dependency com.squareup:kotlinpoet to v1.13.1 --- anvilcodegen/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index 5dbfbec73a..0edb358423 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation(projects.anvilannotations) api(libs.anvil.compiler.api) implementation(libs.anvil.compiler.utils) - implementation("com.squareup:kotlinpoet:1.13.0") + implementation("com.squareup:kotlinpoet:1.13.1") implementation(libs.dagger) compileOnly("com.google.auto.service:auto-service-annotations:1.0.1") kapt("com.google.auto.service:auto-service:1.0.1") From 154bd5e7149af449dfec4464eced77165bdcd22a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 Apr 2023 18:26:32 +0200 Subject: [PATCH 26/33] Update rust-sdk : make the app compiles again --- .../api/notification/NotificationData.kt | 17 +++++-- .../impl/notification/NotificationMapper.kt | 31 +++++------ .../notifications/NotifiableEventResolver.kt | 51 +++++-------------- 3 files changed, 36 insertions(+), 63 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 27fc15c2c6..991f8dd117 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -16,12 +16,19 @@ package io.element.android.libraries.matrix.api.notification -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +//TODO add content data class NotificationData( - val item: MatrixTimelineItem, - val title: String, - val subtitle: String?, + val senderId: UserId, + val eventId: EventId, + val roomId: RoomId, + val senderAvatarUrl: String? = null, + val senderDisplayName: String? = null, + val roomAvatarUrl: String? = null, + val isDirect: Boolean, + val isEncrypted: Boolean, val isNoisy: Boolean, - val avatarUrl: String?, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index ae47beb700..079b1e0a5a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -16,35 +16,28 @@ package io.element.android.libraries.matrix.impl.notification +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationData -import io.element.android.libraries.matrix.impl.timeline.MatrixTimelineItemMapper -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.TimelineEventContentMapper -import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.use import javax.inject.Inject class NotificationMapper @Inject constructor() { - // TODO Inject and remove duplicate? - private val timelineItemFactory = MatrixTimelineItemMapper( - virtualTimelineItemMapper = VirtualTimelineItemMapper(), - eventTimelineItemMapper = EventTimelineItemMapper( - contentMapper = TimelineEventContentMapper( - eventMessageMapper = EventMessageMapper() - ) - ) - ) fun map(notificationItem: NotificationItem): NotificationData { return notificationItem.use { NotificationData( - item = timelineItemFactory.map(it.item), - title = it.title, - subtitle = it.subtitle, - isNoisy = it.isNoisy, - avatarUrl = it.avatarUrl, + senderId = UserId(it.event.senderId()), + eventId = EventId(it.event.eventId()), + roomId = RoomId(it.roomId), + senderAvatarUrl = it.senderAvatarUrl, + senderDisplayName = it.senderDisplayName, + roomAvatarUrl = it.roomAvatarUrl, + isDirect = it.isDirect, + isEncrypted = it.isEncrypted, + isNoisy = it.isNoisy ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 242ca6a67d..82e3e43d50 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -24,11 +24,6 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationData -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails -import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -74,28 +69,28 @@ class NotifiableEventResolver @Inject constructor( } ).orDefault(roomId, eventId) - return notificationData.asNotifiableEvent(sessionId, roomId, eventId) + return notificationData.asNotifiableEvent(sessionId) } } -private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent { +private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent { return NotifiableMessageEvent( sessionId = userId, roomId = roomId, eventId = eventId, editedEventId = null, canBeReplaced = true, - noisy = false, + noisy = isNoisy, timestamp = System.currentTimeMillis(), - senderName = null, - senderId = null, + senderName = senderDisplayName, + senderId = senderId.value, body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…", imageUriString = null, threadId = null, roomName = null, roomIsDirect = false, - roomAvatarPath = null, - senderAvatarPath = null, + roomAvatarPath = roomAvatarUrl, + senderAvatarPath = senderAvatarUrl, soundName = null, outGoingMessage = false, outGoingMessageFailed = false, @@ -109,33 +104,11 @@ private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId */ private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { return this ?: NotificationData( - item = MatrixTimelineItem.Event( - event = EventTimelineItem( - uniqueIdentifier = eventId.value, - eventId = eventId, - isEditable = false, - isLocal = false, - isOwn = false, - isRemote = false, - localSendState = null, - reactions = emptyList(), - sender = UserId("@user:domain"), - senderProfile = ProfileTimelineDetails.Unavailable, - timestamp = System.currentTimeMillis(), - content = MessageContent( - body = eventId.value, - inReplyTo = null, - isEdited = false, - type = TextMessageType( - body = eventId.value, - formatted = null - ) - ) - ), - ), - title = roomId.value, - subtitle = eventId.value, + eventId = eventId, + senderId = UserId("@user:domain"), + roomId = roomId, isNoisy = false, - avatarUrl = null, + isEncrypted = false, + isDirect = false ) } From 9c4407e7b085a2b3328261deddece9ed1f7e829d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Apr 2023 22:29:38 +0000 Subject: [PATCH 27/33] Update dagger to v2.46 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 275145a865..e131db598b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ stem = "2.3.0" sqldelight = "1.5.5" # DI -dagger = "2.45" +dagger = "2.46" anvil = "2.4.5" # quality From 1aa9874f325fd4d53b4a2c76c120a93dd66be4d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 20:29:50 +0000 Subject: [PATCH 28/33] Update android_gradle_plugin to v8.0.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 275145a865..e2642edeb4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ [versions] # Project -android_gradle_plugin = "8.0.0" +android_gradle_plugin = "8.0.1" kotlin = "1.8.20" ksp = "1.8.20-1.0.11" molecule = "0.9.0" From 756371a79bc65c4d06f82b84c947421eba59e9b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 01:03:43 +0000 Subject: [PATCH 29/33] Update dependency com.google.firebase:firebase-bom to v32 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 275145a865..6e2ddfb5f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,7 @@ dependencygraph = "0.10" android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:31.5.0" +google_firebase_bom = "com.google.firebase:firebase-bom:32.0.0" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } From 51760fc2b6fd1ed5ece922cd96d3f36590daf905 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 08:17:37 +0200 Subject: [PATCH 30/33] Update dependency org.jsoup:jsoup to v1.16.1 (#369) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 275145a865..74cb39103f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ coil = "2.3.0" datetime = "0.4.0" serialization_json = "1.5.0" showkase = "1.0.0-beta17" -jsoup = "1.15.4" +jsoup = "1.16.1" appyx = "1.2.0" dependencycheck = "8.2.1" stem = "2.3.0" From b10caa1e06f188a54e891a5b2bf1b4b0f703fc6d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 09:56:20 +0200 Subject: [PATCH 31/33] Update dependency net.zetetic:android-database-sqlcipher to v4.5.4 (#364) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74cb39103f..25ce78229c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -132,7 +132,7 @@ matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.10" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } -sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3" +sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" sqlite = "androidx.sqlite:sqlite:2.3.1" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" gujun_span = "me.gujun.android:span:1.7" From ad7bc5feccb67dbc2cbe83937abf057d31db7c90 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 May 2023 10:55:05 +0200 Subject: [PATCH 32/33] Upgrade gradle/gradle-build-action to v2.4.2 --- .github/workflows/build.yml | 2 +- .github/workflows/quality.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fdc5586c2..be8d1a370c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index dafa94c7b1..e668c17275 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -31,7 +31,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run code quality check suite @@ -79,7 +79,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7646de42a0..391c6c6b21 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v2.4.2 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} From 58e2c93018572d497bf41612cd544136da2cdb6e Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 2 May 2023 13:04:00 +0200 Subject: [PATCH 33/33] Update tests and avoid useless recomposition --- .../roomdetails/impl/RoomDetailsPresenter.kt | 23 ++++++----- .../details/RoomMemberDetailsPresenter.kt | 5 +-- .../roomdetails/RoomDetailsPresenterTests.kt | 38 ++++++++++--------- .../matrix/ui/room/MatrixRoomMembers.kt | 16 +++++++- 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index aa46dacd6d..8f96487583 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -34,7 +34,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.ui.room.directRoomMember +import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -58,9 +58,10 @@ class RoomDetailsPresenter @Inject constructor( LaunchedEffect(Unit) { room.updateMembers() } + val membersState by room.membersStateFlow.collectAsState() val memberCount by getMemberCount(membersState) - val dmMember by room.directRoomMember() + val dmMember by room.getDirectRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType = getRoomType(dmMember) @@ -116,13 +117,15 @@ class RoomDetailsPresenter @Inject constructor( } @Composable - private fun getMemberCount(membersState: MatrixRoomMembersState): State> = remember(membersState) { - derivedStateOf { - when (membersState) { - MatrixRoomMembersState.Unknown -> Async.Uninitialized - is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size) - is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size) - is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size) + private fun getMemberCount(membersState: MatrixRoomMembersState): State> { + return remember(membersState) { + derivedStateOf { + when (membersState) { + MatrixRoomMembersState.Unknown -> Async.Uninitialized + is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size) + is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size) + } } } } @@ -148,3 +151,5 @@ class RoomDetailsPresenter @Inject constructor( } + + diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 904d4f183b..594152e241 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -33,10 +33,9 @@ import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.ui.room.roomMember +import io.element.android.libraries.matrix.ui.room.getRoomMember import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber class RoomMemberDetailsPresenter @AssistedInject constructor( private val client: MatrixClient, @@ -52,7 +51,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( override fun present(): RoomMemberDetailsState { val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } - val roomMember by room.roomMember(roomMemberId) + val roomMember by room.getRoomMember(roomMemberId) // the room member is not really live... val isBlocked = remember { mutableStateOf(roomMember?.isIgnored.orFalse()) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 585a61f05d..2cc6d0ec24 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -86,16 +86,34 @@ class RoomDetailsPresenterTests { @Test fun `present - room member count is calculated asynchronously`() = runTest { + val error = RuntimeException() val room = aMatrixRoom() + val roomMembers = listOf( + aRoomMember(A_USER_ID), + aRoomMember(A_USER_ID_2), + ) val presenter = aRoomDetailsPresenter(room) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { + room.givenRoomMembersState(MatrixRoomMembersState.Unknown) val initialState = awaitItem() Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized) - room.givenRoomMembersState(MatrixRoomMembersState.Ready(emptyList())) - val finalState = awaitItem() - Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) + + room.givenRoomMembersState(MatrixRoomMembersState.Pending(null)) + val loadingState = awaitItem() + Truth.assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null)) + + room.givenRoomMembersState(MatrixRoomMembersState.Error(error)) + //skipItems(1) + val failureState = awaitItem() + Truth.assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null)) + + room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) + //skipItems(1) + val successState = awaitItem() + Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(roomMembers.size)) + cancelAndIgnoreRemainingEvents() } } @@ -136,20 +154,6 @@ class RoomDetailsPresenterTests { } } - @Test - fun `present - can handle error while fetching member count`() = runTest { - val room = aMatrixRoom(name = null).apply { - givenRoomMembersState(MatrixRoomMembersState.Error(Throwable())) - } - val presenter = aRoomDetailsPresenter(room) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java) - cancelAndIgnoreRemainingEvents() - } - } - @Test fun `present - Leave with confirmation on private room shows a specific warning`() = runTest { val room = aMatrixRoom(isPublic = false).apply { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index 2df7c1a152..061ce365eb 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -24,12 +24,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.roomMembers @Composable -fun MatrixRoom.roomMember(userId: UserId): State { +fun MatrixRoom.getRoomMember(userId: UserId): State { val roomMembersState by membersStateFlow.collectAsState() + return getRoomMember(roomMembersState = roomMembersState, userId = userId) +} + +@Composable +fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State { val roomMembers = roomMembersState.roomMembers() return remember(roomMembers) { derivedStateOf { @@ -41,8 +47,13 @@ fun MatrixRoom.roomMember(userId: UserId): State { } @Composable -fun MatrixRoom.directRoomMember(): State { +fun MatrixRoom.getDirectRoomMember(): State { val roomMembersState by membersStateFlow.collectAsState() + return getDirectRoomMember(roomMembersState = roomMembersState) +} + +@Composable +fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): State { val roomMembers = roomMembersState.roomMembers() return remember(roomMembers) { derivedStateOf { @@ -56,3 +67,4 @@ fun MatrixRoom.directRoomMember(): State { } } } +