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 6ef47e4a61..14b90b0064 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -69,6 +69,8 @@ class RoomFlowNode @AssistedInject constructor( private val inputs: Inputs = inputs() + private val roomFlowPresenter = RoomFlowPresenter(inputs.room) + init { lifecycle.subscribe( onCreate = { @@ -110,6 +112,7 @@ class RoomFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { + roomFlowPresenter.present() Children( navModel = backstack, modifier = modifier, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt new file mode 100644 index 0000000000..0a2b066da4 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.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. + */ + +package io.element.android.appnav + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import timber.log.Timber + +class RoomFlowPresenter( + private val room: MatrixRoom, +) : Presenter { + + @Composable + override fun present(): RoomFlowState { + // Preload room members so we can quickly detect if the room is a DM room + LaunchedEffect(Unit) { + room.fetchMembers() + .onFailure { + Timber.e(it, "Fail to fetch members for room ${room.roomId}") + }.onSuccess { + Timber.v("Success fetching members for room ${room.roomId}") + } + } + + return RoomFlowState + } +} + +// At first the return type was Unit, but detekt complained about it +object RoomFlowState diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt new file mode 100644 index 0000000000..1347b0b24c --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt @@ -0,0 +1,62 @@ +/* + * 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.appnav + +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.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.lang.IllegalStateException + +class RoomFlowPresenterTest { + + @Test + fun `present - fetches room members`() = runTest { + val fakeTimeline = FakeMatrixTimeline() + val room = FakeMatrixRoom(matrixTimeline = fakeTimeline) + val presenter = RoomFlowPresenter(room) + + Truth.assertThat(room.areMembersFetched).isFalse() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(room.areMembersFetched).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - recovers from error while fetching room members`() = runTest { + val fakeTimeline = FakeMatrixTimeline() + val room = FakeMatrixRoom(matrixTimeline = fakeTimeline).apply { + givenFetchMemberResult(Result.failure(IllegalStateException("Some error"))) + } + val presenter = RoomFlowPresenter(room) + + Truth.assertThat(room.areMembersFetched).isFalse() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + Truth.assertThat(room.areMembersFetched).isFalse() + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/changelog.d/251.feature b/changelog.d/251.feature index 8c7bb95fd7..209e6e6f71 100644 --- a/changelog.d/251.feature +++ b/changelog.d/251.feature @@ -1 +1,2 @@ Implement Room Details screen +Implement Room Member List screen diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 366cc1e0bd..6f2544822c 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) - implementation(projects.features.selectusers.api) + implementation(projects.features.userlist.api) api(projects.features.createroom.api) testImplementation(libs.test.junit) @@ -56,7 +56,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.features.selectusers.impl) + testImplementation(projects.features.userlist.impl) + testImplementation(projects.features.userlist.test) androidTestImplementation(libs.test.junitext) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt new file mode 100644 index 0000000000..6bbdfb5e93 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt @@ -0,0 +1,33 @@ +/* + * 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.createroom.impl + +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.MatrixUser +import javax.inject.Inject + +// TODO this is empty as we currently don't have an endpoint to perform user search +class AllMatrixUsersDataSource @Inject constructor() : MatrixUserDataSource { + override suspend fun search(query: String): List { + return emptyList() + } + + override suspend fun getProfile(userId: UserId): MatrixUser? { + return null + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index 51b2928862..da51a36335 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -17,31 +17,38 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable -import io.element.android.features.selectusers.api.SelectUsersPresenter -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Presenter import javax.inject.Inject +import javax.inject.Named class AddPeoplePresenter @Inject constructor( - private val selectUsersPresenterFactory: SelectUsersPresenter.Factory, + private val userListPresenterFactory: UserListPresenter.Factory, + @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, ) : Presenter { - private val selectUsersPresenter by lazy { - selectUsersPresenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Multiple)) + private val userListPresenter by lazy { + userListPresenterFactory.create( + UserListPresenterArgs(selectionMode = SelectionMode.Multiple), + matrixUserDataSource, + ) } @Composable override fun present(): AddPeopleState { - val selectUsersState = selectUsersPresenter.present() + val userListState = userListPresenter.present() fun handleEvents(event: AddPeopleEvents) { // do nothing for now } return AddPeopleState( - selectUsersState = selectUsersState, + userListState = userListState, eventSink = ::handleEvents, ) } } + diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt index 8212d02cc4..8605e1aba6 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt @@ -16,9 +16,9 @@ package io.element.android.features.createroom.impl.addpeople -import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.userlist.api.UserListState data class AddPeopleState( - val selectUsersState: SelectUsersState, + val userListState: UserListState, val eventSink: (AddPeopleEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt index 6f1679e252..cfbf7941ce 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt @@ -17,22 +17,22 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.selectusers.api.SelectionMode -import io.element.android.features.selectusers.api.aListOfSelectedUsers -import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.aListOfSelectedUsers +import io.element.android.features.userlist.api.aUserListState open class AddPeopleStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aAddPeopleState(), aAddPeopleState().copy( - selectUsersState = aSelectUsersState().copy( + userListState = aUserListState().copy( selectedUsers = aListOfSelectedUsers(), selectionMode = SelectionMode.Multiple, ) ), aAddPeopleState().copy( - selectUsersState = aSelectUsersState().copy( + userListState = aUserListState().copy( selectedUsers = aListOfSelectedUsers(), isSearchActive = true, selectionMode = SelectionMode.Multiple, @@ -42,6 +42,6 @@ open class AddPeopleStateProvider : PreviewParameterProvider { } fun aAddPeopleState() = AddPeopleState( - selectUsersState = aSelectUsersState(), + userListState = aUserListState(), eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 56b3c975c5..56a16b24f9 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.element.android.features.userlist.api.UserListView import io.element.android.features.createroom.impl.R -import io.element.android.features.selectusers.api.SelectUsersView import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -52,9 +52,9 @@ fun AddPeopleView( Scaffold( topBar = { - if (!state.selectUsersState.isSearchActive) { + if (!state.userListState.isSearchActive) { AddPeopleViewTopBar( - hasSelectedUsers = state.selectUsersState.selectedUsers.isNotEmpty(), + hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, onNextPressed = onNextPressed, ) @@ -66,9 +66,9 @@ fun AddPeopleView( .fillMaxSize() .padding(padding), ) { - SelectUsersView( + UserListView( modifier = Modifier.fillMaxWidth(), - state = state.selectUsersState, + state = state.userListState, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt new file mode 100644 index 0000000000..c5f2d0ca06 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt @@ -0,0 +1,35 @@ +/* + * 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.createroom.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.createroom.impl.AllMatrixUsersDataSource +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.di.AppScope +import javax.inject.Named + +@Module +@ContributesTo(AppScope::class) +interface CreateRoomModule { + + @Binds + @Named("AllUsers") + fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): MatrixUserDataSource + +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 2f3f3ded4a..1250031710 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -17,25 +17,31 @@ package io.element.android.features.createroom.impl.root import androidx.compose.runtime.Composable -import io.element.android.features.selectusers.api.SelectUsersPresenter -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.ui.model.MatrixUser import timber.log.Timber import javax.inject.Inject +import javax.inject.Named class CreateRoomRootPresenter @Inject constructor( - private val presenterFactory: SelectUsersPresenter.Factory, + private val presenterFactory: UserListPresenter.Factory, + @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, ) : Presenter { private val presenter by lazy { - presenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Single)) + presenterFactory.create( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + matrixUserDataSource, + ) } @Composable override fun present(): CreateRoomRootState { - val selectUsersState = presenter.present() + val userListState = presenter.present() fun handleEvents(event: CreateRoomRootEvents) { when (event) { @@ -45,7 +51,7 @@ class CreateRoomRootPresenter @Inject constructor( } return CreateRoomRootState( - selectUsersState = selectUsersState, + userListState = userListState, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt index a57d6aaaf6..d5d75fcfae 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootState.kt @@ -16,9 +16,9 @@ package io.element.android.features.createroom.impl.root -import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.userlist.api.UserListState data class CreateRoomRootState( - val selectUsersState: SelectUsersState, + val userListState: UserListState, val eventSink: (CreateRoomRootEvents) -> Unit, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 678f02476c..d7c18085dc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.selectusers.api.aSelectUsersState +import io.element.android.features.userlist.api.aUserListState open class CreateRoomRootStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,5 +28,5 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider createNode(buildContext) + NavTarget.RoomDetails -> createNode(buildContext, listOf(callback)) + NavTarget.RoomMemberList -> createNode(buildContext) } } 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 8ba373756b..937d755df5 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 @@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -40,6 +41,12 @@ class RoomDetailsNode @AssistedInject constructor( private val room: MatrixRoom, ) : Node(buildContext, plugins = plugins) { + private val callback = plugins().firstOrNull() + + private fun openRoomMemberList() { + callback?.openRoomMemberList() + } + private fun onShareRoom(context: Context) { val alias = room.alias ?: room.alternativeAliases.firstOrNull() val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) } @@ -64,6 +71,7 @@ class RoomDetailsNode @AssistedInject constructor( modifier = modifier, goBack = { navigateUp() }, onShareRoom = { onShareRoom(context) }, + 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 f038787909..80ae4426d9 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,8 +17,16 @@ package io.element.android.features.roomdetails.impl 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.setValue +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject class RoomDetailsPresenter @Inject constructor( @@ -29,13 +37,24 @@ class RoomDetailsPresenter @Inject constructor( override fun present(): RoomDetailsState { // fun handleEvents(event: RoomDetailsEvent) {} + 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) } + ) + } + } + return RoomDetailsState( roomId = room.roomId.value, roomName = room.name ?: room.displayName, roomAlias = room.alias, roomAvatarUrl = room.avatarUrl, roomTopic = room.topic, - memberCount = room.members.size, + memberCount = memberCount, isEncrypted = room.isEncrypted, // 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 ccad10bbc4..78ee70529d 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,13 +16,15 @@ package io.element.android.features.roomdetails.impl +import io.element.android.libraries.architecture.Async + data class RoomDetailsState( val roomId: String, val roomName: String, val roomAlias: String?, val roomAvatarUrl: String?, val roomTopic: String?, - val memberCount: Int, + val memberCount: Async, val isEncrypted: Boolean, // 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 29ecc4f995..c91db4d3a8 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 @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async open class RoomDetailsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,6 +26,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aRoomDetailsState().copy(roomTopic = null), aRoomDetailsState().copy(isEncrypted = false), aRoomDetailsState().copy(roomAlias = null), + aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())), // Add other state here ) } @@ -39,7 +41,7 @@ fun aRoomDetailsState() = RoomDetailsState( "|| MAIL iki/Marketing " + "|| MAI iki/Marketing " + "|| MAI iki/Marketing...", - memberCount = 32, + memberCount = Async.Success(32), isEncrypted = true, // 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 322bc5f8ac..e4d4ac5609 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,6 +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.libraries.architecture.Async +import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -62,6 +64,7 @@ fun RoomDetailsView( state: RoomDetailsState, goBack: () -> Unit, onShareRoom: () -> Unit, + openRoomMemberList: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -87,7 +90,12 @@ fun RoomDetailsView( TopicSection(roomTopic = state.roomTopic) } - MembersSection(memberCount = state.memberCount) + val memberCount = (state.memberCount as? Async.Success)?.state + MembersSection( + memberCount = memberCount, + isLoading = state.memberCount.isLoading(), + openRoomMemberList = openRoomMemberList + ) if (state.isEncrypted) { SecuritySection() @@ -148,12 +156,19 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { } @Composable -internal fun MembersSection(memberCount: Int, modifier: Modifier = Modifier) { +internal fun MembersSection( + memberCount: Int?, + isLoading: Boolean, + modifier: Modifier = Modifier, + openRoomMemberList: () -> Unit +) { PreferenceCategory(modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_people_title), icon = Icons.Outlined.Person, - currentValue = memberCount.toString(), + currentValue = memberCount?.toString(), + onClick = openRoomMemberList, + loadingCurrentValue = isLoading, ) PreferenceText( title = stringResource(R.string.screen_room_details_invite_people_title), @@ -200,5 +215,6 @@ private fun ContentToPreview(state: RoomDetailsState) { state = state, goBack = {}, onShareRoom = {}, + openRoomMemberList = {}, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt new file mode 100644 index 0000000000..49c98374f2 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -0,0 +1,35 @@ +/* + * 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.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.di.RoomScope +import javax.inject.Named + +@Module +@ContributesTo(RoomScope::class) +interface RoomMemberModule { + + @Binds + @Named("RoomMembers") + fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource + +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt new file mode 100644 index 0000000000..b97dcd62e5 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.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.features.roomdetails.impl.members + +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.core.bool.orFalse +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.RoomMember +import io.element.android.libraries.matrix.ui.model.MatrixUser +import javax.inject.Inject + +class RoomMatrixUserDataSource @Inject constructor( + private val room: MatrixRoom +) : MatrixUserDataSource { + + override suspend fun search(query: String): List { + return room.members().filter { member -> + if (query.isBlank()) { + true + } else { + member.userId.contains(query, ignoreCase = true) || member.displayName?.contains(query, ignoreCase = true).orFalse() + } + }.map(::mapMemberToMatrixUser) + } + + override suspend fun getProfile(userId: UserId): MatrixUser? { + return null + } + + private fun mapMemberToMatrixUser(member: RoomMember): MatrixUser { + return MatrixUser( + id = UserId(member.userId), + username = member.displayName, + avatarData = AvatarData( + id = member.userId, + name = member.displayName, + url = member.avatarUrl + ) + ) + } + +} 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 new file mode 100644 index 0000000000..0f65b4657b --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -0,0 +1,52 @@ +/* + * 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.impl.members + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +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.ui.model.MatrixUser +import timber.log.Timber + +@ContributesNode(RoomScope::class) +class RoomMemberListNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomMemberListPresenter, +) : Node(buildContext, plugins = plugins) { + + private fun onUserSelected(matrixUser: MatrixUser) { + Timber.d("TODO: implement user selection. User: $matrixUser") + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RoomMemberListView( + state = state, + modifier = modifier, + onBackPressed = { navigateUp() }, + onUserSelected = ::onUserSelected, + ) + } +} 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 new file mode 100644 index 0000000000..8c3a873ade --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -0,0 +1,64 @@ +/* + * 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.impl.members + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +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.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Named + +class RoomMemberListPresenter @Inject constructor( + private val userListPresenterFactory: UserListPresenter.Factory, + @Named("RoomMembers") private val matrixUserDataSource: MatrixUserDataSource, +) : Presenter { + + private val userListPresenter by lazy { + userListPresenterFactory.create( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + matrixUserDataSource, + ) + } + + @Composable + override fun present(): RoomMemberListState { + val userListState = userListPresenter.present() + val allUsers = remember { mutableStateOf>>(Async.Loading()) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + allUsers.value = Async.Success(matrixUserDataSource.search("").toImmutableList()) + } + } + return RoomMemberListState( + allUsers = allUsers.value, + userListState = userListState + ) + } +} + 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 new file mode 100644 index 0000000000..f5e5bd3efb --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListState.kt @@ -0,0 +1,28 @@ +/* + * 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.impl.members + +import io.element.android.features.userlist.api.UserListState +import io.element.android.libraries.architecture.Async +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, +) 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 new file mode 100644 index 0000000000..fc98ae7544 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -0,0 +1,42 @@ +/* + * 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.impl.members + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.userlist.api.aUserListState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class RoomMemberListStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))), + aRoomMemberListState(allUsers = Async.Loading()) + ) +} + +internal fun aRoomMemberListState( + searchResults: ImmutableList = persistentListOf(), + allUsers: Async> = Async.Uninitialized, +) = + RoomMemberListState( + userListState = aUserListState().copy(searchResults = searchResults), + allUsers = allUsers, + ) 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 new file mode 100644 index 0000000000..e2c41e34b3 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -0,0 +1,145 @@ +/* + * 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.impl.members + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.roomdetails.impl.R +import io.element.android.features.userlist.api.SearchSingleUserResultItem +import io.element.android.features.userlist.api.UserListView +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.isLoading +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar +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.ui.model.MatrixUser + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomMemberListView( + state: RoomMemberListState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onUserSelected: (MatrixUser) -> Unit = {}, +) { + Scaffold( + topBar = { + if (!state.userListState.isSearchActive) { + RoomMemberListTopBar(onBackPressed = onBackPressed) + } + } + ) { padding -> + Column( + modifier = modifier + .fillMaxWidth() + .padding(padding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + UserListView( + state = state.userListState, + onUserSelected = onUserSelected, + ) + + if (!state.userListState.isSearchActive) { + if (state.allUsers is Async.Success) { + LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { + item { + val memberCount = state.allUsers.state.count() + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount), + style = ElementTextStyles.Regular.callout, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Start, + ) + } + items(state.allUsers.state) { matrixUser -> + SearchSingleUserResultItem( + modifier = Modifier.fillMaxWidth(), + matrixUser = matrixUser, + onClick = { onUserSelected(matrixUser) } + ) + } + } + } else if (state.allUsers.isLoading()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomMemberListTopBar( + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.screen_room_details_people_title), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) +} + +@Preview +@Composable +fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomMemberListState) { + RoomMemberListView(state) +} diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index d373ed9bd1..82c0c1c418 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + + "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" 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 f77b606119..7679e1eb7d 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 @@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth 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.room.RoomMember import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -42,8 +43,25 @@ 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(room.members.count()) + Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null)) Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - room member count is calculated asynchronously`() = runTest { + val room = aMatrixRoom() + val presenter = RoomDetailsPresenter(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null)) + + val finalState = awaitItem() + Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0)) } } @@ -56,6 +74,24 @@ class RoomDetailsPresenterTests { }.test { val initialState = awaitItem() Truth.assertThat(initialState.roomName).isEqualTo(room.displayName) + + 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(room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java) + + cancelAndIgnoreRemainingEvents() } } } 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 new file mode 100644 index 0000000000..3564daa5f1 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -0,0 +1,62 @@ +/* + * 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.members + +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.impl.members.RoomMemberListPresenter +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.impl.DefaultUserListPresenter +import io.element.android.features.userlist.test.FakeMatrixUserDataSource +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.coroutines.test.runTest +import okhttp3.internal.toImmutableList +import org.junit.Test + +class RoomMemberListPresenterTests { + + @Test + fun `present - search is done automatically on start, but is async`() = runTest { + val searchResult = listOf(aMatrixUser()) + val userListDataSource = FakeMatrixUserDataSource().apply { + givenSearchResult(searchResult) + } + val userListFactory = object : UserListPresenter.Factory { + override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource) + } + val presenter = RoomMemberListPresenter(userListFactory, userListDataSource) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java) + Truth.assertThat(initialState.userListState.isSearchActive).isFalse() + Truth.assertThat(initialState.userListState.searchResults).isEmpty() + Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single) + + val loadedState = awaitItem() + Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList()) + } + } + +} diff --git a/features/selectusers/api/build.gradle.kts b/features/userlist/api/build.gradle.kts similarity index 93% rename from features/selectusers/api/build.gradle.kts rename to features/userlist/api/build.gradle.kts index d46ed2fbf1..7410de4224 100644 --- a/features/selectusers/api/build.gradle.kts +++ b/features/userlist/api/build.gradle.kts @@ -19,7 +19,7 @@ plugins { } android { - namespace = "io.element.android.features.selectusers.api" + namespace = "io.element.android.features.userlist.api" } dependencies { diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt new file mode 100644 index 0000000000..08eddfd7e9 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt @@ -0,0 +1,25 @@ +/* + * 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.userlist.api + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.MatrixUser + +interface MatrixUserDataSource { + suspend fun search(query: String): List + suspend fun getProfile(userId: UserId): MatrixUser? +} diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt similarity index 68% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt index e0ee6ddf68..f648a14d74 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersEvents.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListEvents.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import io.element.android.libraries.matrix.ui.model.MatrixUser -sealed interface SelectUsersEvents { - data class UpdateSearchQuery(val query: String) : SelectUsersEvents - data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents - data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents - data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents +sealed interface UserListEvents { + data class UpdateSearchQuery(val query: String) : UserListEvents + data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents + data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents + data class OnSearchActiveChanged(val active: Boolean) : UserListEvents } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt similarity index 76% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt index be85455d09..c328efd44e 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenter.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import io.element.android.libraries.architecture.Presenter -interface SelectUsersPresenter : Presenter { +interface UserListPresenter : Presenter { interface Factory { - fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter + fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter } } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt similarity index 88% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt index 543e73b77e..9c8a40504b 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersPresenterArgs.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenterArgs.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api -data class SelectUsersPresenterArgs( +data class UserListPresenterArgs( val selectionMode: SelectionMode, ) diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt similarity index 89% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt index 2a1a2c48e3..80de1e991f 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersState.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import androidx.compose.foundation.lazy.LazyListState import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList -data class SelectUsersState( +data class UserListState( val searchQuery: String, val searchResults: ImmutableList, val selectedUsers: ImmutableList, val selectedUsersListState: LazyListState, val isSearchActive: Boolean, val selectionMode: SelectionMode, - val eventSink: (SelectUsersEvents) -> Unit, + val eventSink: (UserListEvents) -> Unit, ) { val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple } diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt similarity index 82% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt index 93209a632c..d97a4537ed 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersStateProvider.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import androidx.compose.foundation.lazy.LazyListState import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -22,25 +22,25 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.persistentListOf -open class SelectUsersStateProvider : PreviewParameterProvider { - override val values: Sequence +open class UserListStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aSelectUsersState(), - aSelectUsersState().copy( + aUserListState(), + aUserListState().copy( isSearchActive = false, selectedUsers = aListOfSelectedUsers(), selectionMode = SelectionMode.Multiple, ), - aSelectUsersState().copy(isSearchActive = true), - aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone"), - aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), - aSelectUsersState().copy( + aUserListState().copy(isSearchActive = true), + aUserListState().copy(isSearchActive = true, searchQuery = "someone"), + aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), + aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", selectedUsers = aListOfSelectedUsers(), searchResults = aListOfResults(), ), - aSelectUsersState().copy( + aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", selectionMode = SelectionMode.Multiple, @@ -50,7 +50,7 @@ open class SelectUsersStateProvider : PreviewParameterProvider ) } -fun aSelectUsersState() = SelectUsersState( +fun aUserListState() = UserListState( isSearchActive = false, searchQuery = "", searchResults = persistentListOf(), diff --git a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt similarity index 92% rename from features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index 41a5360936..bc355a0a26 100644 --- a/features/selectusers/api/src/main/kotlin/io/element/android/features/selectusers/api/SelectUsersView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.selectusers.api +package io.element.android.features.userlist.api import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -65,8 +65,8 @@ import kotlinx.collections.immutable.ImmutableList import io.element.android.libraries.ui.strings.R as StringR @Composable -fun SelectUsersView( - state: SelectUsersState, +fun UserListView( + state: UserListState, modifier: Modifier = Modifier, onUserSelected: (MatrixUser) -> Unit = {}, onUserDeselected: (MatrixUser) -> Unit = {}, @@ -82,14 +82,14 @@ fun SelectUsersView( selectedUsersListState = state.selectedUsersListState, active = state.isSearchActive, isMultiSelectionEnabled = state.isMultiSelectionEnabled, - onActiveChanged = { state.eventSink(SelectUsersEvents.OnSearchActiveChanged(it)) }, - onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) }, + onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, + onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, onUserSelected = { - state.eventSink(SelectUsersEvents.AddToSelection(it)) + state.eventSink(UserListEvents.AddToSelection(it)) onUserSelected(it) }, onUserDeselected = { - state.eventSink(SelectUsersEvents.RemoveFromSelection(it)) + state.eventSink(UserListEvents.RemoveFromSelection(it)) onUserDeselected(it) }, ) @@ -100,7 +100,7 @@ fun SelectUsersView( modifier = Modifier.padding(16.dp), selectedUsers = state.selectedUsers, onUserRemoved = { - state.eventSink(SelectUsersEvents.RemoveFromSelection(it)) + state.eventSink(UserListEvents.RemoveFromSelection(it)) onUserDeselected(it) }, ) @@ -297,15 +297,15 @@ fun SelectedUser( @Preview @Composable -internal fun SelectUsersViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = +internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun SelectUsersViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = +internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreviewDark { ContentToPreview(state) } @Composable -private fun ContentToPreview(state: SelectUsersState) { - SelectUsersView(state = state) +private fun ContentToPreview(state: UserListState) { + UserListView(state = state) } diff --git a/features/selectusers/impl/build.gradle.kts b/features/userlist/impl/build.gradle.kts similarity index 92% rename from features/selectusers/impl/build.gradle.kts rename to features/userlist/impl/build.gradle.kts index baac7d2d2f..0eaca78a18 100644 --- a/features/selectusers/impl/build.gradle.kts +++ b/features/userlist/impl/build.gradle.kts @@ -23,7 +23,7 @@ plugins { } android { - namespace = "io.element.android.features.selectusers.impl" + namespace = "io.element.android.features.userlist.impl" } anvil { @@ -41,7 +41,7 @@ dependencies { implementation(projects.libraries.elementresources) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) - api(projects.features.selectusers.api) + api(projects.features.userlist.api) ksp(libs.showkase.processor) testImplementation(libs.test.junit) @@ -52,6 +52,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.test.mockk) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.features.userlist.test) androidTestImplementation(libs.test.junitext) } diff --git a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt similarity index 73% rename from features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt rename to features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt index e1135cd1a2..567d183e15 100644 --- a/features/selectusers/impl/src/main/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenter.kt +++ b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.selectusers.impl +package io.element.android.features.userlist.impl import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState @@ -31,10 +31,11 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.selectusers.api.SelectUsersEvents -import io.element.android.features.selectusers.api.SelectUsersPresenter -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectUsersState +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListEvents +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.api.UserListState +import io.element.android.features.userlist.api.UserListPresenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.UserId @@ -45,18 +46,19 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DefaultSelectUsersPresenter @AssistedInject constructor( - @Assisted val args: SelectUsersPresenterArgs, -) : SelectUsersPresenter { +class DefaultUserListPresenter @AssistedInject constructor( + @Assisted val args: UserListPresenterArgs, + @Assisted val matrixUserDataSource: MatrixUserDataSource, +) : UserListPresenter { @AssistedFactory @ContributesBinding(SessionScope::class) - interface DefaultSelectUsersFactory : SelectUsersPresenter.Factory { - override fun create(args: SelectUsersPresenterArgs): DefaultSelectUsersPresenter + interface DefaultUserListFactory : UserListPresenter.Factory { + override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): DefaultUserListPresenter } @Composable - override fun present(): SelectUsersState { + override fun present(): UserListState { val localCoroutineScope = rememberCoroutineScope() var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers: MutableState> = remember { @@ -68,17 +70,17 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( mutableStateOf(persistentListOf()) } - fun handleEvents(event: SelectUsersEvents) { + fun handleEvents(event: UserListEvents) { when (event) { - is SelectUsersEvents.OnSearchActiveChanged -> isSearchActive = event.active - is SelectUsersEvents.UpdateSearchQuery -> searchQuery = event.query - is SelectUsersEvents.AddToSelection -> { + is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active + is UserListEvents.UpdateSearchQuery -> searchQuery = event.query + is UserListEvents.AddToSelection -> { if (event.matrixUser !in selectedUsers.value) { selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList() } localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) } - is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + is UserListEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() } } @@ -95,7 +97,7 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( } } - return SelectUsersState( + return UserListState( searchQuery = searchQuery, searchResults = searchResults.value, selectedUsers = selectedUsers.value.reversed().toImmutableList(), @@ -106,11 +108,11 @@ class DefaultSelectUsersPresenter @AssistedInject constructor( ) } - private fun performSearch(query: String): ImmutableList { + private suspend fun performSearch(query: String): ImmutableList { val isMatrixId = MatrixPatterns.isUserId(query) - val results = mutableListOf()// TODO trigger /search request + val results = matrixUserDataSource.search(query).toMutableList() if (isMatrixId && results.none { it.id.value == query }) { - val getProfileResult: MatrixUser? = null // TODO trigger /profile request + val getProfileResult: MatrixUser? = matrixUserDataSource.getProfile(UserId(query)) val profile = getProfileResult ?: MatrixUser(UserId(query)) results.add(0, profile) } diff --git a/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt similarity index 68% rename from features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt rename to features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt index f5b0b43d0e..1cae186d56 100644 --- a/features/selectusers/impl/src/test/kotlin/io/element/android/features/selectusers/impl/DefaultSelectUsersPresenterTests.kt +++ b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt @@ -14,16 +14,17 @@ * limitations under the License. */ -package io.element.android.features.selectusers.impl +package io.element.android.features.userlist.impl import androidx.compose.foundation.lazy.LazyListState import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.selectusers.api.SelectUsersEvents -import io.element.android.features.selectusers.api.SelectUsersPresenterArgs -import io.element.android.features.selectusers.api.SelectionMode +import io.element.android.features.userlist.api.UserListEvents +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.test.FakeMatrixUserDataSource import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -34,11 +35,16 @@ import kotlinx.coroutines.test.runTest import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class DefaultSelectUsersPresenterTests { +class DefaultUserListPresenterTests { + + private val userListDataSource = FakeMatrixUserDataSource() @Test fun `present - initial state for single selection`() = runTest { - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -53,7 +59,10 @@ class DefaultSelectUsersPresenterTests { @Test fun `present - initial state for multiple selection`() = runTest { - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Multiple)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Multiple), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -68,26 +77,29 @@ class DefaultSelectUsersPresenterTests { @Test fun `present - update search query`() = runTest { - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(true)) + initialState.eventSink(UserListEvents.OnSearchActiveChanged(true)) assertThat(awaitItem().isSearchActive).isTrue() val matrixIdQuery = "@name:matrix.org" - initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(matrixIdQuery)) + initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery)) assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery) assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery))) val notMatrixIdQuery = "name" - initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(notMatrixIdQuery)) + initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery)) assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery) assertThat(awaitItem().searchResults).isEmpty() - initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(false)) + initialState.eventSink(UserListEvents.OnSearchActiveChanged(false)) assertThat(awaitItem().isSearchActive).isFalse() } } @@ -97,7 +109,10 @@ class DefaultSelectUsersPresenterTests { mockkConstructor(LazyListState::class) coJustRun { anyConstructed().scrollToItem(index = any()) } - val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) + val presenter = DefaultUserListPresenter( + UserListPresenterArgs(selectionMode = SelectionMode.Single), + userListDataSource + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -108,23 +123,23 @@ class DefaultSelectUsersPresenterTests { val userABis = aMatrixUser("userA", "A") val userC = aMatrixUser("userC", "C") - initialState.eventSink(SelectUsersEvents.AddToSelection(userA)) + initialState.eventSink(UserListEvents.AddToSelection(userA)) assertThat(awaitItem().selectedUsers).containsExactly(userA) - initialState.eventSink(SelectUsersEvents.AddToSelection(userB)) + initialState.eventSink(UserListEvents.AddToSelection(userB)) // the last added user should be presented first assertThat(awaitItem().selectedUsers).containsExactly(userB, userA) - initialState.eventSink(SelectUsersEvents.AddToSelection(userABis)) - initialState.eventSink(SelectUsersEvents.AddToSelection(userC)) + initialState.eventSink(UserListEvents.AddToSelection(userABis)) + initialState.eventSink(UserListEvents.AddToSelection(userC)) // duplicated users should be ignored assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA) - initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userB)) + initialState.eventSink(UserListEvents.RemoveFromSelection(userB)) assertThat(awaitItem().selectedUsers).containsExactly(userC, userA) - initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userA)) + initialState.eventSink(UserListEvents.RemoveFromSelection(userA)) assertThat(awaitItem().selectedUsers).containsExactly(userC) - initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userC)) + initialState.eventSink(UserListEvents.RemoveFromSelection(userC)) assertThat(awaitItem().selectedUsers).isEmpty() } } diff --git a/features/userlist/test/build.gradle.kts b/features/userlist/test/build.gradle.kts new file mode 100644 index 0000000000..56ac66c154 --- /dev/null +++ b/features/userlist/test/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.userlist.test" +} + +dependencies { + implementation(projects.libraries.matrixui) + implementation(projects.libraries.matrix.api) + api(projects.features.userlist.api) + api(libs.coroutines.core) +} diff --git a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt new file mode 100644 index 0000000000..db6297ec05 --- /dev/null +++ b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.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.features.userlist.test + +import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.MatrixUser + +class FakeMatrixUserDataSource : MatrixUserDataSource { + + private var searchResult: List = emptyList() + private var profile: MatrixUser? = null + + override suspend fun search(query: String): List = searchResult + + override suspend fun getProfile(userId: UserId): MatrixUser? = profile + + fun givenSearchResult(users: List) { + this.searchResult = users + } + + fun givenUserProfile(matrixUser: MatrixUser?) { + this.profile = 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 236ead4b0c..bb74dff2a9 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 @@ -57,3 +57,7 @@ suspend fun (suspend () -> Result).executeResult(state: MutableState Async.isLoading(): Boolean { + return this is Async.Loading +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index 7d8d219cbf..96dfdbaba7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.progressSemantics import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.MaterialTheme @@ -39,6 +41,7 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -47,6 +50,7 @@ fun PreferenceText( modifier: Modifier = Modifier, subtitle: String? = null, currentValue: String? = null, + loadingCurrentValue: Boolean = false, icon: ImageVector? = null, tintColor: Color? = null, onClick: () -> Unit = {}, @@ -56,11 +60,13 @@ fun PreferenceText( modifier = modifier .fillMaxWidth() .defaultMinSize(minHeight = minHeight) - .padding(end = preferencePaddingHorizontal) - .clickable { onClick() }, + .clickable { onClick() } + .padding(end = preferencePaddingHorizontal), ) { Row( - modifier = Modifier.fillMaxWidth().padding(vertical = preferencePaddingVertical) + modifier = Modifier + .fillMaxWidth() + .padding(vertical = preferencePaddingVertical) ) { PreferenceIcon(icon = icon, tintColor = tintColor) Column(modifier = Modifier @@ -88,7 +94,11 @@ fun PreferenceText( if (currentValue != null) { Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary) Spacer(Modifier.width(16.dp)) + } else if (loadingCurrentValue) { + CircularProgressIndicator(modifier = Modifier.progressSemantics().size(20.dp), strokeWidth = 2.dp) + Spacer(Modifier.width(16.dp)) } + } } } 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 07d466d428..bd76d95b35 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 @@ -31,9 +31,11 @@ interface MatrixRoom: Closeable { val alternativeAliases: List val topic: String? val avatarUrl: String? - val members: List val isEncrypted: Boolean + suspend fun members() : List + suspend fun memberCount(): Int + fun syncUpdateFlow(): Flow fun timeline(): MatrixTimeline 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 eb3bc1c7d2..291aa57392 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,6 +17,7 @@ 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.room.MatrixRoom @@ -24,10 +25,12 @@ 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.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 @@ -43,10 +46,32 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, ) : MatrixRoom { + private var loadMembersJob: Job? = null + private var cachedMembers: List = emptyList() + + 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 syncUpdateFlow(): Flow { return slidingSyncUpdateFlow .filter { - it.rooms.contains(innerRoom.id()) + it.rooms.contains(roomId.value) } .map { System.currentTimeMillis() @@ -95,9 +120,6 @@ class RustMatrixRoom( return innerRoom.avatarUrl() } - override val members: List - get() = innerRoom.members().map(RoomMemberMapper::map) - override val isEncrypted: Boolean get() = innerRoom.isEncrypted() 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 15a79c3586..e942f31b76 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 @@ -87,14 +87,6 @@ class RustMatrixTimeline( override fun initialize() { Timber.v("Init timeline for room ${matrixRoom.roomId}") - coroutineScope.launch { - matrixRoom.fetchMembers() - .onFailure { - Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}") - }.onSuccess { - Timber.v("Success fetching members for room ${matrixRoom.roomId}") - } - } coroutineScope.launch { val result = addListener(innerTimelineListener) result 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 32e73121c8..b8ea695f47 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 @@ -34,13 +34,18 @@ class FakeMatrixRoom( override val displayName: String = "", override val topic: String? = null, override val avatarUrl: String? = null, - override val members: List = emptyList(), override val isEncrypted: Boolean = false, override val alias: String? = null, override val alternativeAliases: List = emptyList(), + private val members: List = emptyList(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { + private var fetchMemberResult: Result = Result.success(Unit) + + var areMembersFetched: Boolean = false + private set + override fun syncUpdateFlow(): Flow { return emptyFlow() } @@ -50,7 +55,11 @@ class FakeMatrixRoom( } override suspend fun fetchMembers(): Result { - return Result.success(Unit) + return fetchMemberResult.also { result -> + if (result.isSuccess) { + areMembersFetched = true + } + } } override suspend fun userDisplayName(userId: String): Result { @@ -61,6 +70,18 @@ class FakeMatrixRoom( TODO("Not yet implemented") } + override suspend fun members(): List { + return members + } + + override suspend fun memberCount(): Int { + if (fetchMemberResult.isSuccess) { + return members.count() + } else { + throw fetchMemberResult.exceptionOrNull()!! + } + } + override suspend fun sendMessage(message: String): Result { delay(100) return Result.success(Unit) @@ -94,4 +115,8 @@ class FakeMatrixRoom( } override fun close() = Unit + + fun givenFetchMemberResult(result: Result) { + fetchMemberResult = result + } } diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..6878d7aab4 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -0,0 +1,10 @@ + + + "Confirmare" + "Creați o cameră" + "Gata" + "OK" + "Raportează conținutul" + "Începe discuția" + "Vezi sursa" + \ 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 a28d8bb7bc..7ae90b9b53 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -48,6 +48,7 @@ "About" "Audio" "Bubbles" + "Creating room…" "Decryption error" "Developer options" "(edited)" @@ -122,11 +123,19 @@ "Rageshake to report bug" "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" - "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images." + "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" "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "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" + "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." "Block user" diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_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.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..27f36f9248 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6893108aabbf06f43af90f17b6b1a8a521312559f407647ce08db06ecb9a8f84 +size 22033 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_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.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..32d8a266d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e1f7feb544a84e6e66f995683038abb9e7465583fade4e77c254a4264d03b9f +size 11985 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_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.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b493c070d2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e7b5bd916d4d3067b5013400b4ac864fab560e96fcb75ec895684021a00b8ba +size 21808 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_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.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9ef4e1ee89 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members_null_DefaultGroup_RoomMemberListLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:309c0c0a650435cfbc0825025f6f852bdb51899ff9d37cf4cf9db00d56fb1176 +size 11933 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_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_null_DefaultGroup_RoomDetailsDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a3331e0b02 --- /dev/null +++ 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_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fe9b98fbdd3fd9c7789463f14345f6f65f9a18eedf2229d14c62a3bee6711e3 +size 70058 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_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_null_DefaultGroup_RoomDetailsLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c0511cb21 --- /dev/null +++ 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_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48f138bc8a63ac25152491c974c68d6e024ded04b973f174fb5b9586523217ca +size 64593 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 781807d057..155a42836a 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -61,7 +61,9 @@ { "name": ":features:roomdetails:impl", "includeRegex": [ - "screen_room_details_.*" + "screen_room_details_.*", + "screen_room_member_list_.*", + "screen_dm_details_.*" ] } ]