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
|
|
@ -11,7 +11,11 @@ import android.app.Activity
|
|||
import android.os.Parcelable
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -19,31 +23,43 @@ import com.bumble.appyx.core.node.node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesEntryPoint
|
||||
import io.element.android.features.changeroommemberroes.api.ChangeRoomMemberRolesListType
|
||||
import io.element.android.features.home.api.HomeEntryPoint
|
||||
import io.element.android.features.home.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
||||
import io.element.android.features.invite.api.InviteData
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.appyx.launchMolecule
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class HomeFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val presenter: HomePresenter,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
private val analyticsService: AnalyticsService,
|
||||
|
|
@ -51,6 +67,8 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
private val directLogoutView: DirectLogoutView,
|
||||
private val reportRoomEntryPoint: ReportRoomEntryPoint,
|
||||
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
|
||||
private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint,
|
||||
private val leaveRoomRenderer: LeaveRoomRenderer,
|
||||
) : BaseFlowNode<HomeFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
|
|
@ -59,12 +77,25 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
init {
|
||||
private val stateFlow = launchMolecule { presenter.present() }
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.Home))
|
||||
}
|
||||
)
|
||||
whenChildAttached { commonLifecycle: Lifecycle,
|
||||
changeRoomMemberRolesNode: ChangeRoomMemberRolesEntryPoint.NodeProxy ->
|
||||
commonLifecycle.coroutineScope.launch {
|
||||
changeRoomMemberRolesNode.waitForRoleChanged()
|
||||
withContext(NonCancellable) {
|
||||
backstack.pop()
|
||||
onNewOwnersSelected(changeRoomMemberRolesNode.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
|
|
@ -76,6 +107,9 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class SelectNewOwnersWhenLeavingRoom(val roomId: RoomId) : NavTarget
|
||||
}
|
||||
|
||||
private fun onRoomClick(roomId: RoomId) {
|
||||
|
|
@ -121,11 +155,18 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onSelectNewOwnersWhenLeavingRoom(roomId: RoomId) {
|
||||
backstack.push(NavTarget.SelectNewOwnersWhenLeavingRoom(roomId))
|
||||
}
|
||||
|
||||
private fun onNewOwnersSelected(roomId: RoomId) {
|
||||
stateFlow.value.roomListState.eventSink(RoomListEvents.LeaveRoom(roomId, needsConfirmation = false))
|
||||
}
|
||||
|
||||
fun rootNode(buildContext: BuildContext): Node {
|
||||
return node(buildContext) { modifier ->
|
||||
val state = presenter.present()
|
||||
val state by stateFlow.collectAsState()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
|
||||
HomeView(
|
||||
homeState = state,
|
||||
onRoomClick = this::onRoomClick,
|
||||
|
|
@ -138,15 +179,22 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
onReportRoomClick = this::onReportRoomClick,
|
||||
onDeclineInviteAndBlockUser = this::onDeclineInviteAndBlockUserClick,
|
||||
modifier = modifier,
|
||||
) {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.roomListState.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = this::onRoomClick,
|
||||
onDeclineInviteSuccess = { },
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
|
||||
acceptDeclineInviteView = {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.roomListState.acceptDeclineInviteState,
|
||||
onAcceptInviteSuccess = this::onRoomClick,
|
||||
onDeclineInviteSuccess = { },
|
||||
modifier = Modifier
|
||||
)
|
||||
},
|
||||
leaveRoomView = {
|
||||
leaveRoomRenderer.Render(
|
||||
state = state.roomListState.leaveRoomState,
|
||||
onSelectNewOwners = this::onSelectNewOwnersWhenLeavingRoom,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
)
|
||||
directLogoutView.Render(state.directLogoutState)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,6 +208,13 @@ class HomeFlowNode @AssistedInject constructor(
|
|||
return when (navTarget) {
|
||||
is NavTarget.ReportRoom -> reportRoomEntryPoint.createNode(this, buildContext, navTarget.roomId)
|
||||
is NavTarget.DeclineInviteAndBlockUser -> declineInviteAndBlockUserEntryPoint.createNode(this, buildContext, navTarget.inviteData)
|
||||
is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
|
||||
val room = runBlocking { matrixClient.getJoinedRoom(navTarget.roomId) } ?: error("Room ${navTarget.roomId} not found")
|
||||
changeRoomMemberRolesEntryPoint.builder(this, buildContext)
|
||||
.room(room)
|
||||
.listType(ChangeRoomMemberRolesListType.SelectNewOwnersWhenLeaving)
|
||||
.build()
|
||||
}
|
||||
NavTarget.Root -> rootNode(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
|
|||
import io.element.android.features.home.impl.roomlist.RoomListEvents
|
||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||
import io.element.android.features.home.impl.search.RoomListSearchView
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -78,8 +77,9 @@ fun HomeView(
|
|||
onMenuActionClick: (RoomListMenuAction) -> Unit,
|
||||
onReportRoomClick: (roomId: RoomId) -> Unit,
|
||||
onDeclineInviteAndBlockUser: (roomSummary: RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
acceptDeclineInviteView: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
leaveRoomView: @Composable () -> Unit,
|
||||
) {
|
||||
val state: RoomListState = homeState.roomListState
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
|
@ -108,7 +108,7 @@ fun HomeView(
|
|||
)
|
||||
}
|
||||
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
leaveRoomView()
|
||||
|
||||
HomeScaffold(
|
||||
state = homeState,
|
||||
|
|
@ -304,5 +304,6 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
|
|||
onMenuActionClick = {},
|
||||
onDeclineInviteAndBlockUser = {},
|
||||
acceptDeclineInviteView = {},
|
||||
leaveRoomView = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ fun RoomListContextMenu(
|
|||
},
|
||||
onLeaveRoomClick = {
|
||||
eventSink(RoomListEvents.HideContextMenu)
|
||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId))
|
||||
eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true))
|
||||
},
|
||||
onFavoriteChange = { isFavorite ->
|
||||
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ sealed interface RoomListEvents {
|
|||
|
||||
sealed interface ContextMenuEvents : RoomListEvents
|
||||
data object HideContextMenu : ContextMenuEvents
|
||||
data class LeaveRoom(val roomId: RoomId) : ContextMenuEvents
|
||||
data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : ContextMenuEvents
|
||||
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
|
||||
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
|
||||
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import io.element.android.features.invite.api.SeenInvitesStore
|
|||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent.ShowConfirmation
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -127,7 +127,9 @@ class RoomListPresenter @Inject constructor(
|
|||
is RoomListEvents.HideContextMenu -> {
|
||||
contextMenu.value = RoomListState.ContextMenu.Hidden
|
||||
}
|
||||
is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(ShowConfirmation(event.roomId))
|
||||
is RoomListEvents.LeaveRoom -> {
|
||||
leaveRoomState.eventSink(LeaveRoomEvent.LeaveRoom(event.roomId, needsConfirmation = event.needsConfirmation))
|
||||
}
|
||||
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
||||
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
||||
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import io.element.android.features.home.impl.search.RoomListSearchState
|
|||
import io.element.android.features.home.impl.search.aRoomListSearchState
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
|
|
@ -70,6 +70,12 @@ internal fun aRoomListState(
|
|||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aLeaveRoomState(
|
||||
eventSink: (LeaveRoomEvent) -> Unit = {}
|
||||
) = object : LeaveRoomState {
|
||||
override val eventSink: (LeaveRoomEvent) -> Unit = eventSink
|
||||
}
|
||||
|
||||
internal fun anAcceptDeclineInviteState(
|
||||
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class RoomListContextMenuTest {
|
|||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
RoomListEvents.HideContextMenu,
|
||||
RoomListEvents.LeaveRoom(contextMenu.roomId),
|
||||
RoomListEvents.LeaveRoom(contextMenu.roomId, needsConfirmation = true),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteS
|
|||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
|
|
@ -319,8 +318,8 @@ class RoomListPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID))
|
||||
leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(RoomListEvents.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
leaveRoomEventsRecorder.assertSingle(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,8 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
|||
onMenuActionClick = onMenuActionClick,
|
||||
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,
|
||||
onReportRoomClick = onReportRoomClick,
|
||||
acceptDeclineInviteView = { },
|
||||
acceptDeclineInviteView = {},
|
||||
leaveRoomView = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue