diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 23c35b119a..80c1a06737 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -256,6 +256,10 @@ class LoggedInFlowNode @AssistedInject constructor( } is NavTarget.Room -> { val callback = object : RoomLoadedFlowNode.Callback { + override fun onOpenRoom(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + override fun onForwardedToSingleRoom(roomId: RoomId) { coroutineScope.launch { attachRoom(roomId) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt index 613ed650c8..3cc405d7fd 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt @@ -74,6 +74,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( ), DaggerComponentOwner { interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId) fun onOpenGlobalNotificationSettings() } @@ -134,6 +135,10 @@ class RoomLoadedFlowNode @AssistedInject constructor( override fun onOpenGlobalNotificationSettings() { callbacks.forEach { it.onOpenGlobalNotificationSettings() } } + + override fun onOpenRoom(roomId: RoomId) { + callbacks.forEach { it.onOpenRoom(roomId) } + } } return roomDetailsEntryPoint.nodeBuilder(this, buildContext) .params(RoomDetailsEntryPoint.Params(initialTarget)) diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 0aaac324da..bd1a414294 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -22,6 +22,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import kotlinx.parcelize.Parcelize @@ -42,6 +43,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onOpenGlobalNotificationSettings() + fun onOpenRoom(roomId: RoomId) } interface NodeBuilder { diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index e35526ec37..44cbbb5cbb 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { api(projects.services.apperror.api) implementation(libs.coil.compose) implementation(projects.features.leaveroom.api) + implementation(projects.features.createroom.api) implementation(projects.services.analytics.api) testImplementation(libs.test.junit) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 60e302494e..27ddf47f92 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -41,6 +41,7 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi import io.element.android.libraries.architecture.createNode import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.mediaviewer.api.local.MediaInfo @@ -152,6 +153,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openAvatarPreview(username: String, avatarUrl: String) { backstack.push(NavTarget.MemberAvatarPreview(username, avatarUrl)) } + + override fun onStartDM(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId) } + } } val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback) createNode(buildContext, plugins) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt index 532e1a8a25..b99319812b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt @@ -85,6 +85,7 @@ private fun PreferenceBlockUser( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block)), onClick = { if (!isLoading) eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) }, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, + style = ListItemStyle.Primary, modifier = modifier, ) } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt index ca462c6507..c65b63432e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient @@ -33,10 +34,11 @@ object RoomMemberModule { fun provideRoomMemberDetailsPresenterFactory( matrixClient: MatrixClient, room: MatrixRoom, + startDMAction: StartDMAction, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId) + return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt index 05688c6cf7..75c66f26e2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt @@ -17,6 +17,8 @@ package io.element.android.features.roomdetails.impl.members.details sealed interface RoomMemberDetailsEvents { + data object StartDM : RoomMemberDetailsEvents + data object ClearStartDMState : RoomMemberDetailsEvents data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data object ClearBlockUserError : RoomMemberDetailsEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 8d20ddaf77..3037136f63 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.lifecycle.subscribe @@ -29,9 +30,11 @@ import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.services.analytics.api.AnalyticsService @@ -46,8 +49,9 @@ class RoomMemberDetailsNode @AssistedInject constructor( presenterFactory: RoomMemberDetailsPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - interface Callback: NodeInputs { + interface Callback : NodeInputs { fun openAvatarPreview(username: String, avatarUrl: String) + fun onStartDM(roomId: RoomId) } data class RoomMemberDetailsInput( @@ -84,12 +88,23 @@ class RoomMemberDetailsNode @AssistedInject constructor( } } + fun onStartDM(roomId: RoomId) { + callback.onStartDM(roomId) + } + val state = presenter.present() + + LaunchedEffect(state.startDmActionState) { + if (state.startDmActionState is Async.Success) { + onStartDM(state.startDmActionState.data) + } + } RoomMemberDetailsView( state = state, modifier = modifier, goBack = this::navigateUp, onShareUser = ::onShareUser, + onDMStarted = ::onStartDM, openAvatarPreview = callback::openAvatarPreview, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 3be83a2fef..463fcf186a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -25,13 +25,18 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState @@ -39,9 +44,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( + @Assisted private val roomMemberId: UserId, private val client: MatrixClient, private val room: MatrixRoom, - @Assisted private val roomMemberId: UserId, + private val startDMAction: StartDMAction, ) : Presenter { interface Factory { @@ -53,6 +59,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } val roomMember by room.getRoomMemberAsState(roomMemberId) + val startDmActionState: MutableState> = remember { mutableStateOf(Async.Uninitialized) } // the room member is not really live... val isBlocked: MutableState> = remember(roomMember) { val isIgnored = roomMember?.isIgnored @@ -88,6 +95,14 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( RoomMemberDetailsEvents.ClearBlockUserError -> { isBlocked.value = Async.Success(isBlocked.value.dataOrNull().orFalse()) } + RoomMemberDetailsEvents.StartDM -> { + coroutineScope.launch { + startDMAction.execute(roomMemberId, startDmActionState) + } + } + RoomMemberDetailsEvents.ClearStartDMState -> { + startDmActionState.value = Async.Uninitialized + } } } @@ -108,6 +123,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( userName = userName, avatarUrl = userAvatar, isBlocked = isBlocked.value, + startDmActionState = startDmActionState.value, displayConfirmationDialog = confirmationDialog, isCurrentUser = client.isMe(roomMember?.userId), eventSink = ::handleEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt index 957db2233e..ee9cfe388f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt @@ -17,12 +17,14 @@ package io.element.android.features.roomdetails.impl.members.details import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId data class RoomMemberDetailsState( val userId: String, val userName: String?, val avatarUrl: String?, val isBlocked: Async, + val startDmActionState: Async, val displayConfirmationDialog: ConfirmationDialog?, val isCurrentUser: Boolean, val eventSink: (RoomMemberDetailsEvents) -> Unit diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt index b14b0e3634..bf629e01d8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -37,6 +37,7 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState( userName = "Daniel", avatarUrl = null, isBlocked = Async.Success(false), + startDmActionState = Async.Uninitialized, displayConfirmationDialog = null, isCurrentUser = false, eventSink = {}, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt index 5d9871ace1..279df0c3ea 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt @@ -26,22 +26,33 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection +import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings @OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomMemberDetailsView( state: RoomMemberDetailsState, onShareUser: () -> Unit, + onDMStarted: (RoomId) -> Unit, goBack: () -> Unit, openAvatarPreview: (username: String, url: String) -> Unit, modifier: Modifier = Modifier, @@ -71,31 +82,36 @@ fun RoomMemberDetailsView( Spacer(modifier = Modifier.height(26.dp)) - // TODO implement send DM - // SendMessageSection(onSendMessage = { - // ... - // }) - if (!state.isCurrentUser) { + StartDMSection(onStartDMClicked = { state.eventSink(RoomMemberDetailsEvents.StartDM) }) BlockUserSection(state) BlockUserDialogs(state) } + AsyncView( + async = state.startDmActionState, + progressText = stringResource(CommonStrings.common_starting_chat), + onSuccess = onDMStarted, + errorMessage = { stringResource(CommonStrings.common_error) }, + onRetry = { state.eventSink(RoomMemberDetailsEvents.StartDM) }, + onErrorDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearStartDMState) }, + ) } } } -/* @Composable -private fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { - PreferenceCategory(modifier = modifier) { - PreferenceText( - title = stringResource(CommonStrings.action_send_message), - icon = Icons.Outlined.ChatBubbleOutline, - onClick = onSendMessage, - ) - } +private fun StartDMSection( + onStartDMClicked: () -> Unit, + modifier: Modifier = Modifier +) { + ListItem( + headlineContent = { Text(stringResource(CommonStrings.common_direct_chat)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chat)), + style = ListItemStyle.Primary, + onClick = onStartDMClicked, + modifier = modifier, + ) } - */ @PreviewWithLargeHeight @Composable @@ -113,6 +129,7 @@ private fun ContentToPreview(state: RoomMemberDetailsState) { state = state, onShareUser = {}, goBack = {}, + onDMStarted = {}, openAvatarPreview = { _, _ -> } ) }