Skip to content

Commit 1a33569

Browse files
jmartinespElementBotganfra
authored
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 <[email protected]> Co-authored-by: ganfra <[email protected]>
1 parent dff295e commit 1a33569

File tree

112 files changed

+1334
-510
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+1334
-510
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ licensee {
318318
allow("MIT")
319319
allow("BSD-2-Clause")
320320
allow("BSD-3-Clause")
321+
allow("EPL-1.0")
321322
allowUrl("https://opensource.org/licenses/MIT")
322323
allowUrl("https://developer.android.com/studio/terms.html")
323324
allowUrl("https://www.zetetic.net/sqlcipher/license/")

appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
1414
import io.element.android.libraries.ui.strings.CommonStrings
1515
import kotlinx.coroutines.CoroutineScope
1616
import kotlinx.coroutines.Job
17+
import kotlinx.coroutines.flow.distinctUntilChanged
1718
import kotlinx.coroutines.flow.filter
1819
import kotlinx.coroutines.flow.launchIn
1920
import kotlinx.coroutines.flow.onEach
@@ -28,6 +29,7 @@ class LoggedInEventProcessor @Inject constructor(
2829
fun observeEvents(coroutineScope: CoroutineScope) {
2930
observingJob = roomMembershipObserver.updates
3031
.filter { !it.isUserInRoom }
32+
.distinctUntilChanged()
3133
.onEach {
3234
when (it.change) {
3335
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import extension.setupAnvil
2+
3+
/*
4+
* Copyright 2025 New Vector Ltd.
5+
*
6+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
7+
* Please see LICENSE files in the repository root for full details.
8+
*/
9+
10+
plugins {
11+
id("io.element.android-library")
12+
id("kotlin-parcelize")
13+
}
14+
15+
android {
16+
namespace = "io.element.android.features.changeroommemberroles.api"
17+
}
18+
19+
setupAnvil()
20+
21+
dependencies {
22+
implementation(projects.anvilannotations)
23+
implementation(projects.libraries.architecture)
24+
implementation(projects.libraries.core)
25+
implementation(projects.libraries.matrix.api)
26+
api(projects.libraries.usersearch.api)
27+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.changeroommemberroes.api
9+
10+
import com.bumble.appyx.core.modality.BuildContext
11+
import com.bumble.appyx.core.node.Node
12+
import io.element.android.libraries.architecture.FeatureEntryPoint
13+
import io.element.android.libraries.architecture.NodeInputs
14+
import io.element.android.libraries.matrix.api.core.RoomId
15+
import io.element.android.libraries.matrix.api.room.JoinedRoom
16+
17+
interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint {
18+
fun builder(parentNode: Node, buildContext: BuildContext): Builder
19+
20+
interface Builder {
21+
fun room(room: JoinedRoom): Builder
22+
fun listType(changeRoomMemberRolesListType: ChangeRoomMemberRolesListType): Builder
23+
fun build(): Node
24+
}
25+
26+
interface NodeProxy {
27+
val roomId: RoomId
28+
suspend fun waitForRoleChanged()
29+
}
30+
}
31+
32+
enum class ChangeRoomMemberRolesListType : NodeInputs {
33+
SelectNewOwnersWhenLeaving,
34+
Admins,
35+
Moderators
36+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import extension.setupAnvil
2+
3+
/*
4+
* Copyright 2025 New Vector Ltd.
5+
*
6+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
7+
* Please see LICENSE files in the repository root for full details.
8+
*/
9+
10+
plugins {
11+
id("io.element.android-compose-library")
12+
id("kotlin-parcelize")
13+
}
14+
15+
android {
16+
namespace = "io.element.android.features.changeroommemberroles.impl"
17+
18+
testOptions {
19+
unitTests {
20+
isIncludeAndroidResources = true
21+
}
22+
}
23+
}
24+
25+
setupAnvil()
26+
27+
dependencies {
28+
api(projects.features.changeroommemberroles.api)
29+
implementation(projects.appnav)
30+
implementation(projects.libraries.architecture)
31+
implementation(projects.libraries.core)
32+
implementation(projects.libraries.designsystem)
33+
implementation(projects.libraries.matrix.api)
34+
// For test fixtures used in previews
35+
implementation(projects.libraries.previewutils)
36+
implementation(projects.libraries.matrixui)
37+
implementation(projects.libraries.uiStrings)
38+
implementation(projects.services.analytics.api)
39+
40+
testImplementation(projects.services.analytics.test)
41+
testImplementation(libs.test.junit)
42+
testImplementation(libs.coroutines.test)
43+
testImplementation(libs.molecule.runtime)
44+
testImplementation(libs.test.robolectric)
45+
testImplementation(libs.test.truth)
46+
testImplementation(libs.test.turbine)
47+
testImplementation(projects.libraries.matrix.test)
48+
testImplementation(projects.tests.testutils)
49+
testImplementation(libs.androidx.compose.ui.test.junit)
50+
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
51+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
8+
package io.element.android.features.changeroommemberroles.impl
99

1010
import io.element.android.libraries.matrix.api.user.MatrixUser
1111

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,60 @@
11
/*
2-
* Copyright 2024 New Vector Ltd.
2+
* Copyright 2025 New Vector Ltd.
33
*
44
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
8+
package io.element.android.features.changeroommemberroles.impl
99

10-
import android.os.Parcelable
1110
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.collectAsState
12+
import androidx.compose.runtime.getValue
1213
import androidx.compose.ui.Modifier
1314
import com.bumble.appyx.core.modality.BuildContext
1415
import com.bumble.appyx.core.node.Node
1516
import com.bumble.appyx.core.plugin.Plugin
1617
import dagger.assisted.Assisted
1718
import dagger.assisted.AssistedInject
1819
import io.element.android.anvilannotations.ContributesNode
20+
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
1921
import io.element.android.libraries.architecture.NodeInputs
22+
import io.element.android.libraries.architecture.appyx.launchMolecule
2023
import io.element.android.libraries.architecture.inputs
2124
import io.element.android.libraries.di.RoomScope
2225
import io.element.android.libraries.matrix.api.room.RoomMember
23-
import kotlinx.parcelize.Parcelize
26+
import kotlinx.coroutines.flow.first
2427

2528
@ContributesNode(RoomScope::class)
2629
class ChangeRolesNode @AssistedInject constructor(
2730
@Assisted buildContext: BuildContext,
2831
@Assisted plugins: List<Plugin>,
2932
presenterFactory: ChangeRolesPresenter.Factory,
3033
) : Node(buildContext, plugins = plugins) {
31-
sealed interface ListType : Parcelable {
32-
@Parcelize
33-
data object Admins : ListType
34-
@Parcelize
35-
data object Moderators : ListType
36-
}
37-
38-
@Parcelize
3934
data class Inputs(
40-
val listType: ListType,
41-
) : NodeInputs, Parcelable
35+
val listType: ChangeRoomMemberRolesListType,
36+
) : NodeInputs
4237

4338
private val inputs: Inputs = inputs()
4439

4540
private val presenter = presenterFactory.run {
4641
val role = when (inputs.listType) {
47-
is ListType.Admins -> RoomMember.Role.Admin
48-
is ListType.Moderators -> RoomMember.Role.Moderator
42+
ChangeRoomMemberRolesListType.Admins -> RoomMember.Role.Admin
43+
ChangeRoomMemberRolesListType.Moderators -> RoomMember.Role.Moderator
44+
ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving -> RoomMember.Role.Owner(isCreator = false)
4945
}
5046
create(role)
5147
}
5248

49+
private val stateFlow = launchMolecule { presenter.present() }
50+
51+
suspend fun waitForRoleChanged() {
52+
stateFlow.first { it.savingState.isSuccess() }
53+
}
54+
5355
@Composable
5456
override fun View(modifier: Modifier) {
55-
val state = presenter.present()
57+
val state by stateFlow.collectAsState()
5658
ChangeRolesView(
5759
modifier = modifier,
5860
state = state,
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/*
2-
* Copyright 2024 New Vector Ltd.
2+
* Copyright 2025 New Vector Ltd.
33
*
44
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
8+
package io.element.android.features.changeroommemberroles.impl
99

1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.LaunchedEffect
@@ -22,9 +22,6 @@ import dagger.assisted.Assisted
2222
import dagger.assisted.AssistedFactory
2323
import dagger.assisted.AssistedInject
2424
import im.vector.app.features.analytics.plan.RoomModeration
25-
import io.element.android.features.roomdetails.impl.analytics.toAnalyticsMemberRole
26-
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator
27-
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
2825
import io.element.android.libraries.architecture.AsyncAction
2926
import io.element.android.libraries.architecture.Presenter
3027
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -37,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
3734
import io.element.android.libraries.matrix.api.room.toMatrixUser
3835
import io.element.android.libraries.matrix.api.user.MatrixUser
3936
import io.element.android.libraries.matrix.ui.model.roleOf
37+
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
4038
import io.element.android.services.analytics.api.AnalyticsService
4139
import kotlinx.collections.immutable.ImmutableList
4240
import kotlinx.collections.immutable.PersistentList
@@ -136,8 +134,9 @@ class ChangeRolesPresenter @AssistedInject constructor(
136134
val isModifyingAdmins = role == RoomMember.Role.Admin
137135
val hasChanges = selectedUsers != usersWithRole
138136
val isConfirming = saveState.value.isConfirming()
137+
val modifyingOwners = role is RoomMember.Role.Owner
139138

140-
val needsConfirmation = currentUserIsAdmin && isModifyingAdmins && hasChanges && !isConfirming
139+
val needsConfirmation = (modifyingOwners || currentUserIsAdmin && isModifyingAdmins) && hasChanges && !isConfirming
141140

142141
when {
143142
needsConfirmation -> {
@@ -229,3 +228,10 @@ class ChangeRolesPresenter @AssistedInject constructor(
229228
}
230229
}
231230
}
231+
232+
internal fun RoomMember.Role.toAnalyticsMemberRole(): RoomModeration.Role = when (this) {
233+
is RoomMember.Role.Owner -> RoomModeration.Role.Administrator // TODO - distinguish creator from admin
234+
RoomMember.Role.Admin -> RoomModeration.Role.Administrator
235+
RoomMember.Role.Moderator -> RoomModeration.Role.Moderator
236+
RoomMember.Role.User -> RoomModeration.Role.User
237+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
8+
package io.element.android.features.changeroommemberroles.impl
99

10-
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator
1110
import io.element.android.libraries.architecture.AsyncAction
1211
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
1312
import io.element.android.libraries.matrix.api.core.UserId
1413
import io.element.android.libraries.matrix.api.room.RoomMember
1514
import io.element.android.libraries.matrix.api.user.MatrixUser
15+
import io.element.android.libraries.matrix.ui.room.PowerLevelRoomMemberComparator
1616
import kotlinx.collections.immutable.ImmutableList
1717
import kotlinx.collections.immutable.toImmutableList
1818

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles
8+
package io.element.android.features.changeroommemberroles.impl
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11-
import io.element.android.features.roomdetails.impl.members.aRoomMember
12-
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
1311
import io.element.android.libraries.architecture.AsyncAction
1412
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
1513
import io.element.android.libraries.matrix.api.core.UserId
@@ -18,6 +16,8 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
1816
import io.element.android.libraries.matrix.api.user.MatrixUser
1917
import io.element.android.libraries.matrix.ui.components.aMatrixUser
2018
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
19+
import io.element.android.libraries.previewutils.room.aRoomMember
20+
import io.element.android.libraries.previewutils.room.aRoomMemberList
2121
import kotlinx.collections.immutable.ImmutableList
2222
import kotlinx.collections.immutable.persistentListOf
2323
import kotlinx.collections.immutable.toImmutableList
@@ -44,6 +44,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider<ChangeRolesState> {
4444
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Success(Unit)),
4545
aChangeRolesStateWithSelectedUsers().copy(savingState = AsyncAction.Failure(Exception("boom"))),
4646
aChangeRolesStateWithOwners(),
47+
aChangeRolesStateWithOwners().copy(role = RoomMember.Role.Owner(isCreator = false)),
4748
)
4849
}
4950

0 commit comments

Comments
 (0)