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 4b82cb6992..0f6da36e26 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -42,8 +42,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.room.RoomFlowNode -import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.RoomNavigationTarget +import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService @@ -55,6 +55,7 @@ import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint @@ -83,6 +84,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import timber.log.Timber +import java.util.Optional @ContributesNode(SessionScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -214,6 +216,7 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class Room( val roomId: RoomId, + val roomDescription: RoomDescription? = null, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages ) : NavTarget @@ -304,7 +307,11 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) } } - val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement) + val inputs = RoomFlowNode.Inputs( + roomId = navTarget.roomId, + roomDescription = Optional.ofNullable(navTarget.roomDescription), + initialElement = navTarget.initialElement + ) createNode(buildContext, plugins = listOf(inputs, callback)) } is NavTarget.Settings -> { @@ -375,8 +382,12 @@ class LoggedInFlowNode @AssistedInject constructor( NavTarget.RoomDirectorySearch -> { roomDirectoryEntryPoint.nodeBuilder(this, buildContext) .callback(object : RoomDirectoryEntryPoint.Callback { - override fun onOpenRoom(roomId: RoomId) { - coroutineScope.launch { attachRoom(roomId) } + override fun onRoomJoined(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + + override fun onResultClicked(roomDescription: RoomDescription) { + backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription)) } }) .build() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 91809fe0ca..db3664e631 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -37,6 +37,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.joinroom.api.JoinRoomEntryPoint +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -53,6 +54,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import timber.log.Timber +import java.util.Optional import kotlin.jvm.optionals.getOrNull @ContributesNode(SessionScope::class) @@ -72,6 +74,7 @@ class RoomFlowNode @AssistedInject constructor( ) { data class Inputs( val roomId: RoomId, + val roomDescription: Optional, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, ) : NodeInputs @@ -102,6 +105,7 @@ class RoomFlowNode @AssistedInject constructor( } .launchIn(lifecycleScope) + // When leaving the room from this session only, navigate up. roomMembershipObserver.updates .filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom } .onEach { @@ -114,7 +118,7 @@ class RoomFlowNode @AssistedInject constructor( return when (navTarget) { NavTarget.Loading -> loadingNode(buildContext) NavTarget.JoinRoom -> { - val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId) + val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription) joinRoomEntryPoint.createNode(this, buildContext, inputs) } NavTarget.JoinedRoom -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index ade1f7e147..7d44825fe2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -44,7 +44,6 @@ import io.element.android.libraries.di.SessionScope 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.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -60,10 +59,9 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor( private val appNavigationStateService: AppNavigationStateService, private val appCoroutineScope: CoroutineScope, roomComponentFactory: RoomComponentFactory, - roomMembershipObserver: RoomMembershipObserver, ) : BaseFlowNode( backstack = BackStack( - initialElement = when(plugins.filterIsInstance(Inputs::class.java).first().initialElement){ + initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) { RoomNavigationTarget.Messages -> NavTarget.Messages RoomNavigationTarget.Details -> NavTarget.RoomDetails RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings diff --git a/features/joinroom/api/build.gradle.kts b/features/joinroom/api/build.gradle.kts index 697dc5deee..a016c2d195 100644 --- a/features/joinroom/api/build.gradle.kts +++ b/features/joinroom/api/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.features.roomdirectory.api) } diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt index 50dc1010d8..0047eb10a8 100644 --- a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt +++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt @@ -18,9 +18,11 @@ package io.element.android.features.joinroom.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.matrix.api.core.RoomId +import java.util.Optional interface JoinRoomEntryPoint : FeatureEntryPoint { @@ -28,6 +30,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint { data class Inputs( val roomId: RoomId, + val roomDescription: Optional, ) : NodeInputs } diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 047bc1d960..10b95789ec 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.features.invite.api) + implementation(projects.features.roomdirectory.api) implementation(projects.libraries.uiStrings) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt index a0dcf7a52c..7763c17cad 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt @@ -38,7 +38,7 @@ class JoinRoomNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { private val inputs: JoinRoomEntryPoint.Inputs = inputs() - private val presenter = presenterFactory.create(inputs.roomId) + private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription) @Composable override fun View(modifier: Modifier) { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 39b8058075..f21422a8ce 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -25,6 +25,7 @@ import dagger.assisted.AssistedInject import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.api.response.InviteData +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient @@ -36,34 +37,29 @@ import kotlin.jvm.optionals.getOrNull class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, + @Assisted private val roomDescription: Optional, private val matrixClient: MatrixClient, private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ) : Presenter { interface Factory { - fun create(roomId: RoomId): JoinRoomPresenter + fun create(roomId: RoomId, roomDescription: Optional): JoinRoomPresenter } @Composable override fun present(): JoinRoomState { - val mxRoomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) - val joinAuthorisationStatus = joinAuthorisationStatus(mxRoomInfo) + val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) + val joinAuthorisationStatus = joinAuthorisationStatus(roomInfo) val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() - val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = mxRoomInfo) { + val contentState by produceState>(initialValue = AsyncData.Uninitialized, key1 = roomInfo) { value = when { - mxRoomInfo.isPresent -> { - val roomInfo = mxRoomInfo.get().let { - RoomInfo( - roomId = roomId, - roomName = it.name, - roomAlias = it.canonicalAlias, - memberCount = it.activeMembersCount, - isDirect = it.isDirect, - topic = it.topic, - roomAvatarUrl = it.avatarUrl - ) - } - AsyncData.Success(roomInfo) + roomInfo.isPresent -> { + val contentState = roomInfo.get().toContentState() + AsyncData.Success(contentState) + } + roomDescription.isPresent -> { + val contentState = roomDescription.get().toContentState() + AsyncData.Success(contentState) } else -> AsyncData.Uninitialized } @@ -73,30 +69,68 @@ class JoinRoomPresenter @AssistedInject constructor( when (event) { JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(roomInfo.toInviteData()) + AcceptDeclineInviteEvents.AcceptInvite(contentState.toInviteData()) ) } JoinRoomEvents.DeclineInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(roomInfo.toInviteData()) + AcceptDeclineInviteEvents.DeclineInvite(contentState.toInviteData()) ) } } } return JoinRoomState( - roomInfo = roomInfo, + contentState = contentState, joinAuthorisationStatus = joinAuthorisationStatus, acceptDeclineInviteState = acceptDeclineInviteState, eventSink = ::handleEvents ) } - private fun AsyncData.toInviteData(): InviteData { + private fun RoomDescription.toContentState(): ContentState { + return ContentState( + roomId = roomId, + name = name, + description = description, + numberOfMembers = numberOfMembers, + isDirect = false, + roomAvatarUrl = avatarUrl + ) + } + + private fun MatrixRoomInfo.toContentState(): ContentState { + fun title(): String { + return name ?: canonicalAlias ?: roomId.value + } + + fun description(): String? { + val topic = topic + val alias = canonicalAlias + val name = name + return when { + topic != null -> topic + name != null && alias != null -> alias + name == null && alias == null -> null + else -> roomId.value + } + } + + return ContentState( + roomId = roomId, + name = title(), + description = description(), + numberOfMembers = activeMembersCount, + isDirect = isDirect, + roomAvatarUrl = avatarUrl + ) + } + + private fun AsyncData.toInviteData(): InviteData { return dataOrNull().let { InviteData( roomId = roomId, - roomName = it?.roomName ?: "", + roomName = it?.name ?: "", isDirect = it?.isDirect ?: false ) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index b866ec0d42..73218bf7ba 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -25,27 +25,27 @@ import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class JoinRoomState( - val roomInfo: AsyncData, + val contentState: AsyncData, val joinAuthorisationStatus: JoinAuthorisationStatus, val acceptDeclineInviteState: AcceptDeclineInviteState, val eventSink: (JoinRoomEvents) -> Unit -) { - val showMemberCount = roomInfo.dataOrNull()?.memberCount != null -} +) -data class RoomInfo( +data class ContentState( val roomId: RoomId, - val roomName: String?, - val roomAlias: String?, - val memberCount: Long?, - val topic: String?, + val name: String, + val description: String?, + val numberOfMembers: Long?, val isDirect: Boolean, val roomAvatarUrl: String?, ) { + + val showMemberCount = numberOfMembers != null + fun avatarData(size: AvatarSize): AvatarData { return AvatarData( id = roomId.value, - name = roomName, + name = name, url = roomAvatarUrl, size = size, ) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 7ae82b3ce9..a1c77a7de2 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -26,7 +26,7 @@ open class JoinRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aJoinRoomState( - roomInfo = AsyncData.Uninitialized + contentState = AsyncData.Uninitialized ), aJoinRoomState( joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin @@ -41,14 +41,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider { } fun aJoinRoomState( - roomInfo: AsyncData = AsyncData.Success( - RoomInfo( + contentState: AsyncData = AsyncData.Success( + ContentState( roomId = RoomId("@exa:matrix.org"), - roomName = "Element x android", - roomAlias = "#exa:matrix.org", - memberCount = null, + name = "Element x android", + description = "#exa:matrix.org", + numberOfMembers = null, isDirect = false, - topic = null, roomAvatarUrl = null ) ), @@ -56,7 +55,7 @@ fun aJoinRoomState( acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( - roomInfo = roomInfo, + contentState = contentState, joinAuthorisationStatus = joinAuthorisationStatus, acceptDeclineInviteState = acceptDeclineInviteState, eventSink = eventSink diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index d85ba33edf..519df1a3d6 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api @@ -33,7 +32,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -42,10 +40,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview @@ -66,10 +62,10 @@ fun JoinRoomView( HeaderFooterPage( modifier = modifier, topBar = { - JoinRoomTopBar(asyncRoomInfo = state.roomInfo, onBackClicked = onBackPressed) + JoinRoomTopBar(onBackClicked = onBackPressed) }, content = { - JoinRoomContent(state = state) + JoinRoomContent(asyncContentState = state.contentState) }, footer = { JoinRoomFooter( @@ -127,40 +123,65 @@ private fun JoinRoomFooter( @Composable private fun JoinRoomContent( - state: JoinRoomState, + asyncContentState: AsyncData, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(all = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + + @Composable + fun ContentScaffold( + avatar: @Composable () -> Unit, + title: String, + description: String, + memberCount: @Composable (() -> Unit)? = null ) { - when (state.roomInfo) { - is AsyncData.Success -> { - val roomInfo = state.roomInfo.data - Avatar(avatarData = roomInfo.avatarData(AvatarSize.RoomHeader)) - } - else -> { - PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) - } - } + avatar() Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(id = R.string.screen_join_room_title_no_preview), + text = title, style = ElementTheme.typography.fontHeadingMdBold, textAlign = TextAlign.Center, color = ElementTheme.colors.textPrimary, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.screen_join_room_subtitle_no_preview), + text = description, style = ElementTheme.typography.fontBodyMdRegular, textAlign = TextAlign.Center, color = ElementTheme.colors.textSecondary, ) - if (state.showMemberCount) { - JoinRoomMembersCount(memberCount = state.roomInfo.dataOrNull()?.memberCount ?: 0) + memberCount?.invoke() + } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (asyncContentState) { + is AsyncData.Success -> { + val contentState = asyncContentState.data + ContentScaffold( + avatar = { + Avatar(contentState.avatarData(AvatarSize.RoomHeader)) + }, + title = contentState.name, + description = contentState.description ?: stringResource(R.string.screen_join_room_subtitle_no_preview) + ) { + if (contentState.showMemberCount) { + JoinRoomMembersCount(memberCount = contentState.numberOfMembers ?: 0) + } + } + } + else -> { + ContentScaffold( + avatar = { + PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) + }, + title = stringResource(R.string.screen_join_room_title_no_preview), + description = stringResource(R.string.screen_join_room_subtitle_no_preview), + ) + } } } } @@ -192,7 +213,6 @@ fun JoinRoomMembersCount(memberCount: Long) { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun JoinRoomTopBar( - asyncRoomInfo: AsyncData, onBackClicked: () -> Unit, ) { TopAppBar( @@ -200,44 +220,11 @@ private fun JoinRoomTopBar( BackButton(onClick = onBackClicked) }, title = { - when (asyncRoomInfo) { - is AsyncData.Success -> { - val roomInfo = asyncRoomInfo.data - if(roomInfo.roomName == null){ - IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) - }else { - RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom)) - } - } - else -> { - IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) - } - } + }, ) } -@Composable -private fun RoomAvatarAndNameRow( - roomName: String, - roomAvatar: AvatarData, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar(roomAvatar) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = roomName, - style = ElementTheme.typography.fontBodyLgMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } -} - @PreviewLightDark @Composable fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview { diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index b1e9b04879..ff479d8b74 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -21,10 +21,12 @@ import dagger.Module import dagger.Provides import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter import io.element.android.features.joinroom.impl.JoinRoomPresenter +import io.element.android.features.roomdirectory.api.RoomDescription 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.libraries.matrix.api.roomlist.RoomListService +import java.util.Optional @Module @ContributesTo(SessionScope::class) @@ -36,9 +38,10 @@ object JoinRoomModule { acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { - override fun create(roomId: RoomId): JoinRoomPresenter { + override fun create(roomId: RoomId, roomDescription: Optional): JoinRoomPresenter { return JoinRoomPresenter( roomId = roomId, + roomDescription = roomDescription, matrixClient = client, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, ) diff --git a/features/roomdirectory/api/build.gradle.kts b/features/roomdirectory/api/build.gradle.kts index 04a813bd0f..03430dca7c 100644 --- a/features/roomdirectory/api/build.gradle.kts +++ b/features/roomdirectory/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt index 5b945b2a7d..bc0e9f48c1 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -16,13 +16,26 @@ package io.element.android.features.roomdirectory.api +import android.os.Parcelable import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.Parcelize +@Parcelize data class RoomDescription( val roomId: RoomId, val name: String, val description: String, - val avatarData: AvatarData, + val avatarUrl: String?, val canBeJoined: Boolean, -) + val numberOfMembers: Long, +) : Parcelable { + + fun avatarData(size: AvatarSize) = AvatarData( + id = roomId.value, + name = name, + url = avatarUrl, + size = size, + ) +} diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt index 5a693a4a83..4c90b82543 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -31,6 +31,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onOpenRoom(roomId: RoomId) + fun onRoomJoined(roomId: RoomId) + fun onResultClicked(roomDescription: RoomDescription) } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index dc3581589e..a9e5f2cff5 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -25,6 +25,7 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId @@ -35,9 +36,16 @@ class RoomDirectoryNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { + + private fun onResultClicked(roomDescription: RoomDescription) { + plugins().forEach { + it.onResultClicked(roomDescription) + } + } + private fun onRoomJoined(roomId: RoomId) { plugins().forEach { - it.onOpenRoom(roomId) + it.onRoomJoined(roomId) } } @@ -47,6 +55,7 @@ class RoomDirectoryNode @AssistedInject constructor( RoomDirectoryView( state = state, onRoomJoined = ::onRoomJoined, + onResultClicked = ::onResultClicked, onBackPressed = ::navigateUp, modifier = modifier ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index efb6624260..3178a06322 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -19,8 +19,6 @@ package io.element.android.features.roomdirectory.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roomdirectory.api.RoomDescription 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 import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -71,25 +69,17 @@ fun aRoomDescriptionList(): ImmutableList { roomId = RoomId("!exa:matrix.org"), name = "Element X Android", description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "!exa:matrix.org", - name = "Element X Android", - url = null, - size = AvatarSize.RoomDirectoryItem - ), + avatarUrl = null, canBeJoined = true, + numberOfMembers = 2765, ), RoomDescription( roomId = RoomId("!exi:matrix.org"), name = "Element X iOS", description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "!exi:matrix.org", - name = "Element X iOS", - url = null, - size = AvatarSize.RoomDirectoryItem - ), + avatarUrl = null, canBeJoined = false, + numberOfMembers = 356, ) ) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index f6188c3269..04c4f1dfe1 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -51,6 +51,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.impl.R import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -70,12 +71,13 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun RoomDirectoryView( state: RoomDirectoryState, + onResultClicked: (RoomDescription) -> Unit, onRoomJoined: (RoomId) -> Unit, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - fun joinRoom(roomId: RoomId) { - state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) + fun joinRoom(roomDescription: RoomDescription) { + state.eventSink(RoomDirectoryEvents.JoinRoom(roomDescription.roomId)) } Scaffold( @@ -86,10 +88,11 @@ fun RoomDirectoryView( content = { padding -> RoomDirectoryContent( state = state, - onResultClicked = ::joinRoom, + onResultClicked = onResultClicked, + onJoinClicked = ::joinRoom, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -128,7 +131,8 @@ private fun RoomDirectoryTopBar( @Composable private fun RoomDirectoryContent( state: RoomDirectoryState, - onResultClicked: (RoomId) -> Unit, + onResultClicked: (RoomDescription) -> Unit, + onJoinClicked: (RoomDescription) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -143,6 +147,7 @@ private fun RoomDirectoryContent( displayLoadMoreIndicator = state.displayLoadMoreIndicator, displayEmptyState = state.displayEmptyState, onResultClicked = onResultClicked, + onJoinClicked = onJoinClicked, onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) }, ) } @@ -153,7 +158,8 @@ private fun RoomDirectoryRoomList( roomDescriptions: ImmutableList, displayLoadMoreIndicator: Boolean, displayEmptyState: Boolean, - onResultClicked: (RoomId) -> Unit, + onResultClicked: (RoomDescription) -> Unit, + onJoinClicked: (RoomDescription) -> Unit, onReachedLoadMore: () -> Unit, modifier: Modifier = Modifier, ) { @@ -161,7 +167,12 @@ private fun RoomDirectoryRoomList( items(roomDescriptions) { roomDescription -> RoomDirectoryRoomRow( roomDescription = roomDescription, - onClick = onResultClicked, + onClick = { + onResultClicked(roomDescription) + }, + onJoinClick = { + onJoinClicked(roomDescription) + }, ) } if (displayEmptyState) { @@ -188,10 +199,10 @@ private fun RoomDirectoryRoomList( @Composable private fun LoadMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(24.dp), + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(24.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -256,30 +267,29 @@ private fun SearchTextField( @Composable private fun RoomDirectoryRoomRow( roomDescription: RoomDescription, - onClick: (RoomId) -> Unit, + onClick: () -> Unit, + onJoinClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier - .fillMaxWidth() - .clickable(enabled = roomDescription.canBeJoined) { - onClick(roomDescription.roomId) - } - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable(enabled = roomDescription.canBeJoined, onClick = onClick) + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( - avatarData = roomDescription.avatarData, + avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem), modifier = Modifier.align(Alignment.CenterVertically) ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.name, @@ -301,8 +311,9 @@ private fun RoomDirectoryRoomRow( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 4.dp, end = 12.dp) + .align(Alignment.CenterVertically) + .clickable(onClick = onJoinClick) + .padding(start = 4.dp, end = 12.dp) ) } else { Spacer(modifier = Modifier.width(24.dp)) @@ -315,6 +326,7 @@ private fun RoomDirectoryRoomRow( internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview { RoomDirectoryView( state = state, + onResultClicked = {}, onRoomJoined = {}, onBackPressed = {}, ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index be36eb5053..5ace7645aa 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -42,12 +42,8 @@ fun MatrixRoomDescription.toFeatureModel(): RoomDescription { roomId = roomId, name = name(), description = description(), - avatarData = AvatarData( - id = roomId.value, - name = name, - url = avatarUrl, - size = AvatarSize.RoomDirectoryItem, - ), + avatarUrl = avatarUrl, + numberOfMembers = numberOfMembers, canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC, ) } diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index bcac35fc3a..fe69ace945 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -105,7 +105,7 @@ private fun AndroidComposeTestRule.setRoomD setContent { RoomDirectoryView( state = state, - onRoomJoined = onRoomJoined, + onRoomClicked = onRoomJoined, onBackPressed = onBackPressed, ) }