Merge pull request #5758 from element-hq/feature/bma/fixNPE
Fix null pointer exception on room notification settings.
This commit is contained in:
commit
bbf6529c1e
13 changed files with 149 additions and 108 deletions
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
|
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
|
@ -49,6 +50,10 @@ class EditDefaultNotificationSettingPresenter(
|
||||||
fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter
|
fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val collator = Collator.getInstance().apply {
|
||||||
|
decomposition = Collator.CANONICAL_DECOMPOSITION
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): EditDefaultNotificationSettingState {
|
override fun present(): EditDefaultNotificationSettingState {
|
||||||
var displayMentionsOnlyDisclaimer by remember { mutableStateOf(false) }
|
var displayMentionsOnlyDisclaimer by remember { mutableStateOf(false) }
|
||||||
|
|
@ -121,10 +126,10 @@ class EditDefaultNotificationSettingPresenter(
|
||||||
summaries: List<RoomSummary>,
|
summaries: List<RoomSummary>,
|
||||||
roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>
|
roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>
|
||||||
) {
|
) {
|
||||||
val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
|
val roomWithUserDefinedRules: Set<RoomId> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
|
||||||
roomsWithUserDefinedMode.value = summaries
|
roomsWithUserDefinedMode.value = summaries
|
||||||
.filter { roomSummary ->
|
.filter { roomSummary ->
|
||||||
roomWithUserDefinedRules.contains(roomSummary.roomId.value) && roomSummary.isOneToOne == isOneToOne
|
roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isOneToOne == isOneToOne
|
||||||
}
|
}
|
||||||
.map { roomSummary ->
|
.map { roomSummary ->
|
||||||
EditNotificationSettingRoomInfo(
|
EditNotificationSettingRoomInfo(
|
||||||
|
|
@ -138,7 +143,12 @@ class EditDefaultNotificationSettingPresenter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// locale sensitive sorting
|
// locale sensitive sorting
|
||||||
.sortedWith(compareBy(Collator.getInstance()) { roomSummary -> roomSummary.name })
|
.sortedWith(
|
||||||
|
compareBy(collator) { roomSummary ->
|
||||||
|
// Collator does not handle null values, so we provide a fallback
|
||||||
|
roomSummary.name ?: roomSummary.roomId.value
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {
|
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
|
|
||||||
|
|
@ -8,33 +8,34 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.notifications
|
package io.element.android.features.preferences.impl.notifications
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
|
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
|
||||||
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
|
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class EditDefaultNotificationSettingsPresenterTest {
|
class EditDefaultNotificationSettingsPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - ensures initial state is correct`() = runTest {
|
fun `present - ensures initial state is correct`() = runTest {
|
||||||
val notificationSettingsService = FakeNotificationSettingsService()
|
val notificationSettingsService = FakeNotificationSettingsService(
|
||||||
|
getRoomsWithUserDefinedRulesResult = { Result.success(emptyList()) },
|
||||||
|
)
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.mode).isNull()
|
assertThat(initialState.mode).isNull()
|
||||||
assertThat(initialState.isOneToOne).isFalse()
|
assertThat(initialState.isOneToOne).isFalse()
|
||||||
|
assertThat(initialState.roomsWithUserDefinedMode).isEmpty()
|
||||||
|
|
||||||
val loadedState = consumeItemsUntilPredicate {
|
val loadedState = consumeItemsUntilPredicate {
|
||||||
it.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
it.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
|
||||||
|
|
@ -49,13 +50,12 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
fun `present - ensure list of rooms with user defined mode`() = runTest {
|
fun `present - ensure list of rooms with user defined mode`() = runTest {
|
||||||
val notificationSettingsService = FakeNotificationSettingsService(
|
val notificationSettingsService = FakeNotificationSettingsService(
|
||||||
initialRoomMode = RoomNotificationMode.ALL_MESSAGES,
|
initialRoomMode = RoomNotificationMode.ALL_MESSAGES,
|
||||||
initialRoomModeIsDefault = false
|
initialRoomModeIsDefault = false,
|
||||||
|
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID)) },
|
||||||
)
|
)
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
|
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
|
||||||
val loadedState = consumeItemsUntilPredicate { state ->
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||||
|
|
@ -64,12 +64,78 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ensure list of rooms is sorted`() = runTest {
|
||||||
|
val notificationSettingsService = FakeNotificationSettingsService(
|
||||||
|
initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
|
initialRoomModeIsDefault = false,
|
||||||
|
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||||
|
presenter.test {
|
||||||
|
roomListService.postAllRooms(
|
||||||
|
listOf(
|
||||||
|
aRoomSummary(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = "Z",
|
||||||
|
userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
|
),
|
||||||
|
aRoomSummary(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
name = "A",
|
||||||
|
userDefinedNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
|
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }
|
||||||
|
}.last()
|
||||||
|
assertThat(loadedState.roomsWithUserDefinedMode[0].name).isEqualTo("A")
|
||||||
|
assertThat(loadedState.roomsWithUserDefinedMode[1].name).isEqualTo("Z")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - ensure list of rooms is sorted, with name null`() = runTest {
|
||||||
|
val notificationSettingsService = FakeNotificationSettingsService(
|
||||||
|
initialRoomMode = RoomNotificationMode.MUTE,
|
||||||
|
initialRoomModeIsDefault = false,
|
||||||
|
getRoomsWithUserDefinedRulesResult = { Result.success(listOf(A_ROOM_ID, A_ROOM_ID_2)) },
|
||||||
|
)
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||||
|
presenter.test {
|
||||||
|
roomListService.postAllRooms(
|
||||||
|
listOf(
|
||||||
|
aRoomSummary(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = "Z",
|
||||||
|
userDefinedNotificationMode = RoomNotificationMode.MUTE,
|
||||||
|
),
|
||||||
|
aRoomSummary(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
name = null,
|
||||||
|
userDefinedNotificationMode = RoomNotificationMode.MUTE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val loadedState = consumeItemsUntilPredicate { state ->
|
||||||
|
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.MUTE }
|
||||||
|
}.last()
|
||||||
|
assertThat(loadedState.roomsWithUserDefinedMode[0].name).isNull()
|
||||||
|
assertThat(loadedState.roomsWithUserDefinedMode[1].name).isEqualTo("Z")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - edit default notification setting`() = runTest {
|
fun `present - edit default notification setting`() = runTest {
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter()
|
val presenter = createEditDefaultNotificationSettingPresenter(
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
notificationSettingsService = FakeNotificationSettingsService(
|
||||||
presenter.present()
|
getRoomsWithUserDefinedRulesResult = { Result.success(emptyList()) },
|
||||||
}.test {
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES))
|
awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES))
|
||||||
val loadedState = consumeItemsUntilPredicate {
|
val loadedState = consumeItemsUntilPredicate {
|
||||||
it.mode == RoomNotificationMode.ALL_MESSAGES
|
it.mode == RoomNotificationMode.ALL_MESSAGES
|
||||||
|
|
@ -80,12 +146,12 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - edit default notification setting failed`() = runTest {
|
fun `present - edit default notification setting failed`() = runTest {
|
||||||
val notificationSettingsService = FakeNotificationSettingsService()
|
val notificationSettingsService = FakeNotificationSettingsService(
|
||||||
|
getRoomsWithUserDefinedRulesResult = { Result.success(emptyList()) },
|
||||||
|
)
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
||||||
notificationSettingsService.givenSetDefaultNotificationModeError(AN_EXCEPTION)
|
notificationSettingsService.givenSetDefaultNotificationModeError(AN_EXCEPTION)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES))
|
awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES))
|
||||||
val errorState = consumeItemsUntilPredicate {
|
val errorState = consumeItemsUntilPredicate {
|
||||||
it.changeNotificationSettingAction.isFailure()
|
it.changeNotificationSettingAction.isFailure()
|
||||||
|
|
@ -101,13 +167,13 @@ class EditDefaultNotificationSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - display mentions only warning if homeserver does not support it`() = runTest {
|
fun `present - display mentions only warning if homeserver does not support it`() = runTest {
|
||||||
val notificationSettingsService = FakeNotificationSettingsService().apply {
|
val notificationSettingsService = FakeNotificationSettingsService(
|
||||||
|
getRoomsWithUserDefinedRulesResult = { Result.success(emptyList()) },
|
||||||
|
).apply {
|
||||||
givenCanHomeServerPushEncryptedEventsToDeviceResult(Result.success(false))
|
givenCanHomeServerPushEncryptedEventsToDeviceResult(Result.success(false))
|
||||||
}
|
}
|
||||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
assertThat(awaitLastSequentialItem().displayMentionsOnlyDisclaimer).isTrue()
|
assertThat(awaitLastSequentialItem().displayMentionsOnlyDisclaimer).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ class ChangeRolesPresenter(
|
||||||
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
fun create(role: RoomMember.Role): ChangeRolesPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val powerLevelRoomMemberComparator = PowerLevelRoomMemberComparator()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChangeRolesState {
|
override fun present(): ChangeRolesState {
|
||||||
val dataSource = remember { RoomMemberListDataSource(room, dispatchers) }
|
val dataSource = remember { RoomMemberListDataSource(room, dispatchers) }
|
||||||
|
|
@ -176,17 +178,7 @@ class ChangeRolesPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<RoomMember>.groupedByRole(): MembersByRole {
|
private fun List<RoomMember>.groupedByRole(): MembersByRole {
|
||||||
val groupedMembers = MembersByRole(this)
|
return MembersByRole(this, powerLevelRoomMemberComparator)
|
||||||
return MembersByRole(
|
|
||||||
owners = groupedMembers.owners.sorted(),
|
|
||||||
admins = groupedMembers.admins.sorted(),
|
|
||||||
moderators = groupedMembers.moderators.sorted(),
|
|
||||||
members = groupedMembers.members.sorted(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Iterable<RoomMember>.sorted(): ImmutableList<RoomMember> {
|
|
||||||
return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.save(
|
private fun CoroutineScope.save(
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class ChangeRolesState(
|
data class ChangeRolesState(
|
||||||
|
|
@ -30,21 +30,24 @@ data class ChangeRolesState(
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MembersByRole(
|
data class MembersByRole(
|
||||||
val owners: ImmutableList<RoomMember>,
|
val owners: ImmutableList<RoomMember> = persistentListOf(),
|
||||||
val admins: ImmutableList<RoomMember>,
|
val admins: ImmutableList<RoomMember> = persistentListOf(),
|
||||||
val moderators: ImmutableList<RoomMember>,
|
val moderators: ImmutableList<RoomMember> = persistentListOf(),
|
||||||
val members: ImmutableList<RoomMember>,
|
val members: ImmutableList<RoomMember> = persistentListOf(),
|
||||||
) {
|
) {
|
||||||
constructor(members: List<RoomMember>) : this(
|
constructor(members: List<RoomMember>, comparator: Comparator<RoomMember>) : this(
|
||||||
owners = members.filter { it.role is RoomMember.Role.Owner }.sorted(),
|
owners = members.filterAndSort(comparator) { it.role is RoomMember.Role.Owner },
|
||||||
admins = members.filter { it.role == RoomMember.Role.Admin }.sorted(),
|
admins = members.filterAndSort(comparator) { it.role == RoomMember.Role.Admin },
|
||||||
moderators = members.filter { it.role == RoomMember.Role.Moderator }.sorted(),
|
moderators = members.filterAndSort(comparator) { it.role == RoomMember.Role.Moderator },
|
||||||
members = members.filter { it.role == RoomMember.Role.User }.sorted(),
|
members = members.filterAndSort(comparator) { it.role == RoomMember.Role.User },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun isEmpty() = owners.isEmpty() && admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
fun isEmpty() = owners.isEmpty() && admins.isEmpty() && moderators.isEmpty() && members.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Iterable<RoomMember>.sorted(): ImmutableList<RoomMember> {
|
private fun Iterable<RoomMember>.filterAndSort(
|
||||||
return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList()
|
comparator: Comparator<RoomMember>,
|
||||||
|
predicate: (RoomMember) -> Boolean,
|
||||||
|
): ImmutableList<RoomMember> {
|
||||||
|
return filter(predicate).sortedWith(comparator).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||||
import io.element.android.libraries.previewutils.room.aRoomMember
|
import io.element.android.libraries.previewutils.room.aRoomMember
|
||||||
import io.element.android.libraries.previewutils.room.aRoomMemberList
|
import io.element.android.libraries.previewutils.room.aRoomMemberList
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -36,7 +37,12 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
|
||||||
aChangeRolesStateWithSelectedUsers().copy(
|
aChangeRolesStateWithSelectedUsers().copy(
|
||||||
query = "Alice",
|
query = "Alice",
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchResults = SearchBarResultState.Results(MembersByRole(aRoomMemberList().take(1).toImmutableList())),
|
searchResults = SearchBarResultState.Results(
|
||||||
|
MembersByRole(
|
||||||
|
members = aRoomMemberList().take(1),
|
||||||
|
comparator = PowerLevelRoomMemberComparator(),
|
||||||
|
)
|
||||||
|
),
|
||||||
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
selectedUsers = aMatrixUserList().take(1).toImmutableList(),
|
||||||
),
|
),
|
||||||
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation),
|
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.ConfirmingCancellation),
|
||||||
|
|
@ -87,7 +93,8 @@ internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState(
|
||||||
} else {
|
} else {
|
||||||
roomMember
|
roomMember
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
comparator = PowerLevelRoomMemberComparator(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
hasPendingChanges = true,
|
hasPendingChanges = true,
|
||||||
|
|
@ -126,7 +133,8 @@ internal fun aChangeRolesStateWithOwners(
|
||||||
displayName = "David",
|
displayName = "David",
|
||||||
role = RoomMember.Role.User,
|
role = RoomMember.Role.User,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
comparator = PowerLevelRoomMemberComparator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
canRemoveMember = { userId ->
|
canRemoveMember = { userId ->
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ fun ChangeRolesView(
|
||||||
SearchResultsList(
|
SearchResultsList(
|
||||||
currentRole = state.role,
|
currentRole = state.role,
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: MembersByRole(emptyList()),
|
searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: MembersByRole(),
|
||||||
selectedUsers = state.selectedUsers,
|
selectedUsers = state.selectedUsers,
|
||||||
canRemoveMember = state.canChangeMemberRole,
|
canRemoveMember = state.canChangeMemberRole,
|
||||||
onToggleSelection = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it.toMatrixUser())) },
|
onToggleSelection = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it.toMatrixUser())) },
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_5
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_6
|
import io.element.android.libraries.matrix.test.A_USER_ID_6
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_7
|
import io.element.android.libraries.matrix.test.A_USER_ID_7
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ class MembersByRoleTest {
|
||||||
aRoomMember(A_USER_ID_6, displayName = "Justin", role = RoomMember.Role.Owner(isCreator = true)),
|
aRoomMember(A_USER_ID_6, displayName = "Justin", role = RoomMember.Role.Owner(isCreator = true)),
|
||||||
aRoomMember(A_USER_ID_7, displayName = "Mallory", role = RoomMember.Role.Owner(isCreator = false)),
|
aRoomMember(A_USER_ID_7, displayName = "Mallory", role = RoomMember.Role.Owner(isCreator = false)),
|
||||||
)
|
)
|
||||||
val membersByRole = MembersByRole(members = members)
|
val membersByRole = MembersByRole(members = members, comparator = PowerLevelRoomMemberComparator())
|
||||||
assertThat(membersByRole.owners).containsExactly(
|
assertThat(membersByRole.owners).containsExactly(
|
||||||
aRoomMember(A_USER_ID_6, displayName = "Justin", role = RoomMember.Role.Owner(isCreator = true)),
|
aRoomMember(A_USER_ID_6, displayName = "Justin", role = RoomMember.Role.Owner(isCreator = true)),
|
||||||
aRoomMember(A_USER_ID_7, displayName = "Mallory", role = RoomMember.Role.Owner(isCreator = false)),
|
aRoomMember(A_USER_ID_7, displayName = "Mallory", role = RoomMember.Role.Owner(isCreator = false)),
|
||||||
|
|
@ -52,37 +53,25 @@ class MembersByRoleTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isEmpty - only returns true with no members of any role`() {
|
fun `isEmpty - only returns true with no members of any role`() {
|
||||||
val emptyMembersByRole = MembersByRole(emptyList())
|
val emptyMembersByRole = MembersByRole()
|
||||||
assertThat(emptyMembersByRole.isEmpty()).isTrue()
|
assertThat(emptyMembersByRole.isEmpty()).isTrue()
|
||||||
|
|
||||||
val membersByRoleWithOwners = MembersByRole(
|
val membersByRoleWithOwners = MembersByRole(
|
||||||
owners = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Admin)),
|
owners = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Admin)),
|
||||||
admins = persistentListOf(),
|
|
||||||
moderators = persistentListOf(),
|
|
||||||
members = persistentListOf(),
|
|
||||||
)
|
)
|
||||||
assertThat(membersByRoleWithOwners.isEmpty()).isFalse()
|
assertThat(membersByRoleWithOwners.isEmpty()).isFalse()
|
||||||
|
|
||||||
val membersByRoleWithAdmins = MembersByRole(
|
val membersByRoleWithAdmins = MembersByRole(
|
||||||
owners = persistentListOf(),
|
|
||||||
admins = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Admin)),
|
admins = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Admin)),
|
||||||
moderators = persistentListOf(),
|
|
||||||
members = persistentListOf(),
|
|
||||||
)
|
)
|
||||||
assertThat(membersByRoleWithAdmins.isEmpty()).isFalse()
|
assertThat(membersByRoleWithAdmins.isEmpty()).isFalse()
|
||||||
|
|
||||||
val membersByRoleWithModerators = MembersByRole(
|
val membersByRoleWithModerators = MembersByRole(
|
||||||
owners = persistentListOf(),
|
|
||||||
admins = persistentListOf(),
|
|
||||||
moderators = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Moderator)),
|
moderators = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.Moderator)),
|
||||||
members = persistentListOf(),
|
|
||||||
)
|
)
|
||||||
assertThat(membersByRoleWithModerators.isEmpty()).isFalse()
|
assertThat(membersByRoleWithModerators.isEmpty()).isFalse()
|
||||||
|
|
||||||
val membersByRoleWithMembers = MembersByRole(
|
val membersByRoleWithMembers = MembersByRole(
|
||||||
owners = persistentListOf(),
|
|
||||||
admins = persistentListOf(),
|
|
||||||
moderators = persistentListOf(),
|
|
||||||
members = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.User)),
|
members = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.User)),
|
||||||
)
|
)
|
||||||
assertThat(membersByRoleWithMembers.isEmpty()).isFalse()
|
assertThat(membersByRoleWithMembers.isEmpty()).isFalse()
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import io.element.android.libraries.matrix.ui.room.sortingName
|
|
||||||
import java.text.Collator
|
|
||||||
|
|
||||||
// Comparator used to sort room members by power level (descending) and then by name (ascending)
|
|
||||||
internal class PowerLevelRoomMemberComparator : Comparator<RoomMember> {
|
|
||||||
// Used to simplify and compare unicode and ASCII chars (á == a)
|
|
||||||
private val collator = Collator.getInstance().apply {
|
|
||||||
decomposition = Collator.CANONICAL_DECOMPOSITION
|
|
||||||
}
|
|
||||||
override fun compare(o1: RoomMember, o2: RoomMember): Int {
|
|
||||||
return when {
|
|
||||||
o1.powerLevel > o2.powerLevel -> return -1
|
|
||||||
o1.powerLevel < o2.powerLevel -> return 1
|
|
||||||
else -> {
|
|
||||||
collator.compare(o1.sortingName(), o2.sortingName())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
|
||||||
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
|
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
|
@ -52,7 +53,8 @@ class RoomMemberListPresenter(
|
||||||
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
private val roomMembersModerationPresenter: Presenter<RoomMemberModerationState>,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
) : Presenter<RoomMemberListState> {
|
) : Presenter<RoomMemberListState> {
|
||||||
var roomMembers: AsyncData<RoomMembers> by mutableStateOf(AsyncData.Loading())
|
private var roomMembers: AsyncData<RoomMembers> by mutableStateOf(AsyncData.Loading())
|
||||||
|
private val powerLevelRoomMemberComparator = PowerLevelRoomMemberComparator()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMemberListState {
|
override fun present(): RoomMemberListState {
|
||||||
|
|
@ -103,7 +105,7 @@ class RoomMemberListPresenter(
|
||||||
.map { it.withIdentityState(roomMemberIdentityStates) }
|
.map { it.withIdentityState(roomMemberIdentityStates) }
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
||||||
.sortedWith(PowerLevelRoomMemberComparator())
|
.sortedWith(powerLevelRoomMemberComparator)
|
||||||
.map { it.withIdentityState(roomMemberIdentityStates) }
|
.map { it.withIdentityState(roomMemberIdentityStates) }
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
banned = members.getOrDefault(RoomMembershipState.BAN, emptyList())
|
banned = members.getOrDefault(RoomMembershipState.BAN, emptyList())
|
||||||
|
|
@ -133,7 +135,7 @@ class RoomMemberListPresenter(
|
||||||
.map { it.withIdentityState(roomMemberIdentityStates) }
|
.map { it.withIdentityState(roomMemberIdentityStates) }
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList())
|
||||||
.sortedWith(PowerLevelRoomMemberComparator())
|
.sortedWith(powerLevelRoomMemberComparator)
|
||||||
.map { it.withIdentityState(roomMemberIdentityStates) }
|
.map { it.withIdentityState(roomMemberIdentityStates) }
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
banned = results.getOrDefault(RoomMembershipState.BAN, emptyList())
|
banned = results.getOrDefault(RoomMembershipState.BAN, emptyList())
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ interface NotificationSettingsService {
|
||||||
suspend fun setCallEnabled(enabled: Boolean): Result<Unit>
|
suspend fun setCallEnabled(enabled: Boolean): Result<Unit>
|
||||||
suspend fun isInviteForMeEnabled(): Result<Boolean>
|
suspend fun isInviteForMeEnabled(): Result<Boolean>
|
||||||
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
|
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
|
||||||
suspend fun getRoomsWithUserDefinedRules(): Result<List<String>>
|
suspend fun getRoomsWithUserDefinedRules(): Result<List<RoomId>>
|
||||||
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
|
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
|
||||||
suspend fun getRawPushRules(): Result<String?>
|
suspend fun getRawPushRules(): Result<String?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,9 @@ class RustNotificationSettingsService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRoomsWithUserDefinedRules(): Result<List<String>> =
|
override suspend fun getRoomsWithUserDefinedRules(): Result<List<RoomId>> =
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
notificationSettings.await().getRoomsWithUserDefinedRules(enabled = true)
|
notificationSettings.await().getRoomsWithUserDefinedRules(enabled = true).map(::RoomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> =
|
override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> =
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE
|
import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -26,6 +25,7 @@ class FakeNotificationSettingsService(
|
||||||
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||||
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||||
private val getRawPushRulesResult: () -> Result<String> = { lambdaError() },
|
private val getRawPushRulesResult: () -> Result<String> = { lambdaError() },
|
||||||
|
private val getRoomsWithUserDefinedRulesResult: () -> Result<List<RoomId>> = { lambdaError() },
|
||||||
) : NotificationSettingsService {
|
) : NotificationSettingsService {
|
||||||
private val notificationSettingsStateFlow = MutableStateFlow(Unit)
|
private val notificationSettingsStateFlow = MutableStateFlow(Unit)
|
||||||
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
|
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
|
||||||
|
|
@ -154,8 +154,8 @@ class FakeNotificationSettingsService(
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRoomsWithUserDefinedRules(): Result<List<String>> {
|
override suspend fun getRoomsWithUserDefinedRules(): Result<List<RoomId>> {
|
||||||
return Result.success(if (roomNotificationModeIsDefault) listOf() else listOf(A_ROOM_ID.value))
|
return getRoomsWithUserDefinedRulesResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> {
|
override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.libraries.matrix.ui.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
|
@ -14,9 +14,10 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class PowerLevelBaseRoomMemberComparatorTest {
|
class PowerLevelRoomMemberComparatorTest {
|
||||||
@Test
|
@Test
|
||||||
fun `order is Admin, then Moderator, then User`() {
|
fun `order is Admin, then Moderator, then User`() {
|
||||||
val memberList = listOf(
|
val memberList = listOf(
|
||||||
Loading…
Add table
Add a link
Reference in a new issue