Merge pull request #2663 from element-hq/feature/bma/testChangeRolesView
Fix a bunch of small issues around moderation and test change roles view
This commit is contained in:
@@ -29,9 +29,11 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
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.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -47,14 +49,22 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||
override fun present(): RolesAndPermissionsState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val roomMembers by room.membersStateFlow.collectAsState()
|
||||
// Get the list of joined room members, in order to filter members present in the power
|
||||
// level state Event, but not member of the room anymore.
|
||||
val joinedRoomMemberIds by remember {
|
||||
derivedStateOf {
|
||||
roomMembers.joinedRoomMembers().map { it.userId }
|
||||
}
|
||||
}
|
||||
val moderatorCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(RoomMember.Role.MODERATOR)
|
||||
roomInfo.userCountWithRole(joinedRoomMemberIds, RoomMember.Role.MODERATOR)
|
||||
}
|
||||
}
|
||||
val adminCount by remember {
|
||||
derivedStateOf {
|
||||
roomInfo.userCountWithRole(RoomMember.Role.ADMIN)
|
||||
roomInfo.userCountWithRole(joinedRoomMemberIds, RoomMember.Role.ADMIN)
|
||||
}
|
||||
}
|
||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
@@ -108,11 +118,9 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int {
|
||||
return if (this != null) {
|
||||
userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role }
|
||||
} else {
|
||||
0
|
||||
private fun MatrixRoomInfo?.userCountWithRole(joinedRoomMemberIds: List<UserId>, role: RoomMember.Role): Int {
|
||||
return this?.userPowerLevels.orEmpty().count { (userId, level) ->
|
||||
RoomMember.Role.forPowerLevel(level) == role && userId in joinedRoomMemberIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface ChangeRolesEvent {
|
||||
data object ToggleSearchActive : ChangeRolesEvent
|
||||
data class QueryChanged(val query: String?) : ChangeRolesEvent
|
||||
data class UserSelectionToggled(val roomMember: RoomMember) : ChangeRolesEvent
|
||||
data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent
|
||||
data object Save : ChangeRolesEvent
|
||||
data object Exit : ChangeRolesEvent
|
||||
data object CancelExit : ChangeRolesEvent
|
||||
|
||||
@@ -42,6 +42,8 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -129,11 +131,11 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
}
|
||||
is ChangeRolesEvent.UserSelectionToggled -> {
|
||||
val newList = selectedUsers.value.toMutableList()
|
||||
val index = newList.indexOfFirst { it.userId == event.roomMember.userId }
|
||||
val index = newList.indexOfFirst { it.userId == event.matrixUser.userId }
|
||||
if (index >= 0) {
|
||||
newList.removeAt(index)
|
||||
} else {
|
||||
newList.add(event.roomMember.toMatrixUser())
|
||||
newList.add(event.matrixUser)
|
||||
}
|
||||
selectedUsers.value = newList.toImmutableList()
|
||||
}
|
||||
@@ -183,12 +185,6 @@ class ChangeRolesPresenter @AssistedInject constructor(
|
||||
return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList()
|
||||
}
|
||||
|
||||
private fun RoomMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
private fun CoroutineScope.save(
|
||||
usersWithRole: ImmutableList<MatrixUser>,
|
||||
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||
|
||||
@@ -62,6 +62,7 @@ internal fun aChangeRolesState(
|
||||
exitState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
savingState: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
canRemoveMember: (UserId) -> Boolean = { true },
|
||||
eventSink: (ChangeRolesEvent) -> Unit = {},
|
||||
) = ChangeRolesState(
|
||||
role = role,
|
||||
query = query,
|
||||
@@ -72,7 +73,7 @@ internal fun aChangeRolesState(
|
||||
exitState = exitState,
|
||||
savingState = savingState,
|
||||
canChangeMemberRole = canRemoveMember,
|
||||
eventSink = {},
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState(
|
||||
|
||||
@@ -46,7 +46,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
@@ -68,6 +67,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
@@ -83,12 +83,8 @@ fun ChangeRolesView(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val updatedOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
|
||||
BackHandler {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(ChangeRolesEvent.ToggleSearchActive)
|
||||
} else {
|
||||
state.eventSink(ChangeRolesEvent.Exit)
|
||||
}
|
||||
BackHandler(enabled = !state.isSearchActive) {
|
||||
state.eventSink(ChangeRolesEvent.Exit)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
@@ -129,7 +125,9 @@ fun ChangeRolesView(
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
SearchBar(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
placeHolderTitle = stringResource(CommonStrings.common_search_for_someone),
|
||||
query = state.query.orEmpty(),
|
||||
onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) },
|
||||
@@ -143,7 +141,7 @@ fun ChangeRolesView(
|
||||
searchResults = members,
|
||||
selectedUsers = state.selectedUsers,
|
||||
canRemoveMember = state.canChangeMemberRole,
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) },
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it.toMatrixUser())) },
|
||||
selectedUsersList = {},
|
||||
)
|
||||
}
|
||||
@@ -159,13 +157,13 @@ fun ChangeRolesView(
|
||||
searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: persistentListOf(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
canRemoveMember = state.canChangeMemberRole,
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) },
|
||||
onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it.toMatrixUser())) },
|
||||
selectedUsersList = { users ->
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
selectedUsers = users,
|
||||
onUserRemoved = {
|
||||
state.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(it.userId)))
|
||||
state.eventSink(ChangeRolesEvent.UserSelectionToggled(it))
|
||||
},
|
||||
canDeselect = { state.canChangeMemberRole(it.userId) },
|
||||
)
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -80,29 +81,35 @@ fun ChangeRoomPermissionsView(
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
for ((index, permissionItem) in state.items.withIndex()) {
|
||||
ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0)
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.ADMIN,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.MODERATOR,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.USER,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
item {
|
||||
ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0)
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.ADMIN,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.MODERATOR,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
SelectRoleItem(
|
||||
permissionsItem = permissionItem,
|
||||
role = RoomMember.Role.USER,
|
||||
currentPermissions = state.currentPermissions
|
||||
) { item, role ->
|
||||
state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.RoomModeration
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesPresenter
|
||||
@@ -30,6 +29,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
@@ -154,10 +154,10 @@ class ChangeRolesPresenterTests {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
assertThat(awaitItem().selectedUsers).hasSize(2)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
assertThat(awaitItem().selectedUsers).hasSize(1)
|
||||
}
|
||||
}
|
||||
@@ -177,13 +177,13 @@ class ChangeRolesPresenterTests {
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedUsers).hasSize(2)
|
||||
assertThat(hasPendingChanges).isTrue()
|
||||
}
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedUsers).hasSize(1)
|
||||
assertThat(hasPendingChanges).isFalse()
|
||||
@@ -226,7 +226,7 @@ class ChangeRolesPresenterTests {
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Exit)
|
||||
val confirmingState = awaitItem()
|
||||
@@ -252,7 +252,7 @@ class ChangeRolesPresenterTests {
|
||||
assertThat(initialState.hasPendingChanges).isFalse()
|
||||
assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
val updatedState = awaitItem()
|
||||
assertThat(updatedState.hasPendingChanges).isTrue()
|
||||
skipItems(1)
|
||||
@@ -279,8 +279,7 @@ class ChangeRolesPresenterTests {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
val confirmingState = awaitItem()
|
||||
assertThat(confirmingState.savingState).isEqualTo(AsyncAction.Confirming)
|
||||
@@ -304,7 +303,7 @@ class ChangeRolesPresenterTests {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
val confirmingState = awaitItem()
|
||||
@@ -334,7 +333,7 @@ class ChangeRolesPresenterTests {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit))
|
||||
@@ -357,7 +356,7 @@ class ChangeRolesPresenterTests {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedUsers).hasSize(1)
|
||||
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2)))
|
||||
initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2)))
|
||||
|
||||
awaitItem().eventSink(ChangeRolesEvent.Save)
|
||||
val failedState = awaitItem()
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.rolesandpermissions.changeroles
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesState
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesView
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.aChangeRolesState
|
||||
import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.aChangeRolesStateWithSelectedUsers
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChangeRolesViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `click on back icon search not active emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.Exit,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on back icon search active emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
isSearchActive = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
// This event should be there, maybe a problem with the SearchBar
|
||||
// It's working fine in the app, so let's ignore it for now
|
||||
// eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on search bar emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_search_for_someone)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
// This event should be there, maybe a problem with the SearchBar
|
||||
// It's working fine in the app, so let's ignore it for now
|
||||
// ChangeRolesEvent.ToggleSearchActive,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on save button emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
hasPendingChanges = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.Save,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing exit confirmation dialog ok emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
exitState = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.Exit,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing exit confirmation dialog cancel emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
exitState = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.CancelExit
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing saving dialog failure OK emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
savingState = AsyncAction.Failure(Exception("boom")),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.ClearError,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing saving confirmation dialog for admin OK emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.ADMIN,
|
||||
savingState = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.Save,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing saving confirmation dialog for admin cancel emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesState(
|
||||
role = RoomMember.Role.ADMIN,
|
||||
savingState = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.ClearError,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing removing user from selected list emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
val selectedUsers = aMatrixUserList().take(2)
|
||||
val userToDeselect = selectedUsers[1]
|
||||
assertThat(userToDeselect.displayName).isEqualTo("Bob")
|
||||
rule.setChangeRolesView(
|
||||
state = aChangeRolesStateWithSelectedUsers().copy(
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
// Unselect the user from the row list
|
||||
val contentDescription = rule.activity.getString(CommonStrings.action_remove)
|
||||
rule.onNodeWithContentDescription(contentDescription).performClick()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.UserSelectionToggled(userToDeselect),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing adding user to the selected list emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
val selectedUsers = aMatrixUserList().take(2)
|
||||
val state = aChangeRolesStateWithSelectedUsers().copy(
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val userToSelect = (state.searchResults as SearchBarResultState.Results).results[2].toMatrixUser()
|
||||
assertThat(userToSelect.displayName).isEqualTo("Carol")
|
||||
rule.setChangeRolesView(
|
||||
state = state,
|
||||
)
|
||||
// Select the user from the rom list
|
||||
rule.onNodeWithText("Carol").performClick()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.UserSelectionToggled(userToSelect),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing removing user to the selected list emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
val selectedUsers = aMatrixUserList().take(2)
|
||||
val state = aChangeRolesStateWithSelectedUsers().copy(
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val userToSelect = (state.searchResults as SearchBarResultState.Results).results[1].toMatrixUser()
|
||||
assertThat(userToSelect.displayName).isEqualTo("Bob")
|
||||
rule.setChangeRolesView(
|
||||
state = state,
|
||||
)
|
||||
// Select the user from the rom list
|
||||
rule.onAllNodesWithText("Bob")[1].performClick()
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
ChangeRolesEvent.QueryChanged(""),
|
||||
ChangeRolesEvent.UserSelectionToggled(userToSelect),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setChangeRolesView(
|
||||
state: ChangeRolesState,
|
||||
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
ChangeRolesView(
|
||||
state = state,
|
||||
onBackPressed = onBackPressed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -35,13 +35,8 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
@@ -183,18 +178,6 @@ interface MatrixRoom : Closeable {
|
||||
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
|
||||
canUserSendState(userId, StateEventType.CALL_MEMBER)
|
||||
|
||||
fun usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
|
||||
return roomInfoFlow
|
||||
.map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } }
|
||||
.distinctUntilChanged()
|
||||
.combine(membersStateFlow) { powerLevels, membersState ->
|
||||
membersState.roomMembers()
|
||||
.orEmpty()
|
||||
.filter { powerLevels.containsKey(it.userId) }
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
|
||||
|
||||
suspend fun removeAvatar(): Result<Unit>
|
||||
|
||||
@@ -35,3 +35,7 @@ fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? {
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun MatrixRoomMembersState.joinedRoomMembers(): List<RoomMember> {
|
||||
return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class RoomMember(
|
||||
val userId: UserId,
|
||||
@@ -78,3 +79,9 @@ enum class RoomMembershipState {
|
||||
fun RoomMember.getBestName(): String {
|
||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||
}
|
||||
|
||||
fun RoomMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.powerlevels
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Return a flow of the list of room members who are still in the room (with membership == RoomMembershipState.JOIN)
|
||||
* and who have the given role.
|
||||
*/
|
||||
fun MatrixRoom.usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
|
||||
return roomInfoFlow
|
||||
.map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } }
|
||||
.combine(membersStateFlow) { powerLevels, membersState ->
|
||||
membersState.joinedRoomMembers()
|
||||
.filter { powerLevels.containsKey(it.userId) }
|
||||
.toPersistentList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
@@ -42,7 +42,7 @@ suspend fun MatrixRoom.canInvite(): Result<Boolean> = canUserInvite(sessionId)
|
||||
suspend fun MatrixRoom.canKick(): Result<Boolean> = canUserKick(sessionId)
|
||||
|
||||
/**
|
||||
* Shortcut for calling [MatrixRoom.canBanUser] with our own user.
|
||||
* Shortcut for calling [MatrixRoom.canUserBan] with our own user.
|
||||
*/
|
||||
suspend fun MatrixRoom.canBan(): Result<Boolean> = canUserBan(sessionId)
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ fun RoomSelectView(
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||
query = state.query,
|
||||
onQueryChange = { state.eventSink(RoomSelectEvents.UpdateQuery(it)) },
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user