Force last owner of a room to pass ownership when leaving (#5094)
* Move `ChangeRoles*` classes to their own module so they can be shared * Hook the change roles screen to the leave room action, add confirmation dialogs * Use enum instead of sealed interface for `ChangeRoomMemberRolesListType` * Try to improve communications between nodes * refactor (leave room) : makes sure to expose only necessary code from api module * Add `:libraries:previewutils` module to share some test fixtures used for UI previews * Update screenshots --------- Co-authored-by: ElementBot <android@element.io> Co-authored-by: ganfra <francoisg@matrix.org>
This commit is contained in:
parent
a87bbdd91c
commit
955263bee1
112 changed files with 1337 additions and 513 deletions
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
api(libs.dagger)
|
||||
api(libs.appyx.core)
|
||||
api(libs.androidx.lifecycle.runtime)
|
||||
api(libs.molecule.runtime)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(InternalComposeApi::class)
|
||||
|
||||
package io.element.android.libraries.architecture.appyx
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.InternalComposeApi
|
||||
import androidx.compose.runtime.currentComposer
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.cash.molecule.AndroidUiDispatcher
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.launchMolecule
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
fun <State> Node.launchMolecule(body: @Composable () -> State): StateFlow<State> {
|
||||
val scope = CoroutineScope(lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
|
||||
return scope.launchMolecule(mode = RecompositionMode.ContextClock) {
|
||||
currentComposer.startProviders(
|
||||
values = arrayOf(LocalLifecycleOwner provides this),
|
||||
)
|
||||
val state = body()
|
||||
currentComposer.endProviders()
|
||||
state
|
||||
}
|
||||
}
|
||||
|
|
@ -9,21 +9,33 @@ package io.element.android.libraries.matrix.api.room.powerlevels
|
|||
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.activeRoomMembers
|
||||
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.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
/**
|
||||
* Return a flow of the list of active room members who have the given role.
|
||||
*/
|
||||
fun BaseRoom.usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
|
||||
// Ensure the room members flow is ready
|
||||
val readyMembersFlow = membersStateFlow
|
||||
.onStart {
|
||||
if (membersStateFlow.value is RoomMembersState.Unknown) {
|
||||
updateMembers()
|
||||
}
|
||||
}
|
||||
.filter { it is RoomMembersState.Ready }
|
||||
|
||||
return roomInfoFlow
|
||||
.map { roomInfo -> roomInfo.usersWithRole(role) }
|
||||
.combine(membersStateFlow) { powerLevels, membersState ->
|
||||
.combine(readyMembersFlow) { powerLevels, membersState ->
|
||||
membersState.activeRoomMembers()
|
||||
.filter { powerLevels.contains(it.userId) }
|
||||
.toPersistentList()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.test.room
|
|||
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.RoomMembershipState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
fun aRoomMember(
|
||||
userId: UserId = UserId("@alice:server.org"),
|
||||
|
|
@ -34,3 +35,31 @@ fun aRoomMember(
|
|||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
)
|
||||
|
||||
fun aRoomMemberList() = persistentListOf(
|
||||
anAlice(),
|
||||
aBob(),
|
||||
aRoomMember(UserId("@carol:server.org"), "Carol"),
|
||||
aRoomMember(UserId("@david:server.org"), "David"),
|
||||
aRoomMember(UserId("@eve:server.org"), "Eve"),
|
||||
aRoomMember(UserId("@justin:server.org"), "Justin"),
|
||||
aRoomMember(UserId("@mallory:server.org"), "Mallory"),
|
||||
aRoomMember(UserId("@susie:server.org"), "Susie"),
|
||||
aVictor(),
|
||||
aWalter(),
|
||||
)
|
||||
|
||||
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin)
|
||||
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator)
|
||||
|
||||
fun aVictor() = aRoomMember(
|
||||
UserId("@victor:server.org"),
|
||||
"Victor",
|
||||
membership = RoomMembershipState.INVITE
|
||||
)
|
||||
|
||||
fun aWalter() = aRoomMember(
|
||||
UserId("@walter:server.org"),
|
||||
"Walter",
|
||||
membership = RoomMembershipState.INVITE
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 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.libraries.matrix.ui.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import java.text.Collator
|
||||
|
||||
// Comparator used to sort room members by power level (descending) and then by name (ascending)
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
libraries/previewutils/build.gradle.kts
Normal file
21
libraries/previewutils/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2022-2024 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.previewutils"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 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.libraries.previewutils.room
|
||||
|
||||
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.RoomMembershipState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
fun aRoomMember(
|
||||
userId: UserId = UserId("@alice:server.org"),
|
||||
displayName: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
powerLevel: Long = 0L,
|
||||
normalizedPowerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.User,
|
||||
membershipChangeReason: String? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
isIgnored = isIgnored,
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
)
|
||||
|
||||
fun aRoomMemberList() = persistentListOf(
|
||||
anAlice(),
|
||||
aBob(),
|
||||
aRoomMember(UserId("@carol:server.org"), "Carol"),
|
||||
aRoomMember(UserId("@david:server.org"), "David"),
|
||||
aRoomMember(UserId("@eve:server.org"), "Eve"),
|
||||
aRoomMember(UserId("@justin:server.org"), "Justin"),
|
||||
aRoomMember(UserId("@mallory:server.org"), "Mallory"),
|
||||
aRoomMember(UserId("@susie:server.org"), "Susie"),
|
||||
aVictor(),
|
||||
aWalter(),
|
||||
)
|
||||
|
||||
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin)
|
||||
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator)
|
||||
|
||||
fun aVictor() = aRoomMember(
|
||||
UserId("@victor:server.org"),
|
||||
"Victor",
|
||||
membership = RoomMembershipState.INVITE
|
||||
)
|
||||
|
||||
fun aWalter() = aRoomMember(
|
||||
UserId("@walter:server.org"),
|
||||
"Walter",
|
||||
membership = RoomMembershipState.INVITE
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue