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 dd53d6168b..beef8a883c 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 @@ -34,9 +34,9 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.appnav.room.join.JoinRoomNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode -import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -64,16 +64,14 @@ class RoomFlowNode @AssistedInject constructor( @Assisted plugins: List, private val roomListService: RoomListService, private val roomMembershipObserver: RoomMembershipObserver, - private val networkMonitor: NetworkMonitor, -) : - BaseFlowNode( - backstack = BackStack( - initialElement = NavTarget.Loading, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins - ) { +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Loading, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { data class Inputs( val roomId: RoomId, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages, @@ -96,18 +94,16 @@ class RoomFlowNode @AssistedInject constructor( super.onBuilt() roomListService.getUserMembershipForRoom( inputs.roomId - ).onEach { membership -> - Timber.d("RoomMembership = $membership") - when { - membership.getOrNull() == CurrentUserMembership.JOINED -> { + ).flowOn(Dispatchers.Default) + .onEach { membership -> + Timber.d("RoomMembership = $membership") + if (membership.getOrNull() == CurrentUserMembership.JOINED) { backstack.newRoot(NavTarget.JoinedRoom) - } - else -> { + } else { backstack.newRoot(NavTarget.JoinRoom) } } - } - .flowOn(Dispatchers.Default) + .flowOn(Dispatchers.Main) .launchIn(lifecycleScope) roomMembershipObserver.updates @@ -121,7 +117,10 @@ class RoomFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Loading -> loadingNode(buildContext) - NavTarget.JoinRoom -> joinRoomNode(buildContext) + NavTarget.JoinRoom -> { + val inputs = JoinRoomNode.Inputs(inputs.roomId) + createNode(buildContext, plugins = listOf(inputs)) + } NavTarget.JoinedRoom -> { val roomFlowNodeCallback = plugins() val inputs = JoinedRoomFlowNode.Inputs(inputs.roomId, initialElement = inputs.initialElement) @@ -136,16 +135,8 @@ class RoomFlowNode @AssistedInject constructor( } } - private fun joinRoomNode(buildContext: BuildContext) = node(buildContext) { - Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Unknown Room") - } - } - @Composable override fun View(modifier: Modifier) { - BackstackView( - transitionHandler = JumpToEndTransitionHandler(), - ) + BackstackView(transitionHandler = JumpToEndTransitionHandler()) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt new file mode 100644 index 0000000000..7679e77c63 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join + +sealed interface JoinRoomEvents { + data object JoinRoom: JoinRoomEvents + data object AcceptInvite : JoinRoomEvents + data object DeclineInvite : JoinRoomEvents +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt new file mode 100644 index 0000000000..cccc66ddfb --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomNode.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId + +@ContributesNode(SessionScope::class) +class JoinRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: JoinRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val roomId: RoomId, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.roomId) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + JoinRoomView( + state = state, + onBackPressed = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt new file mode 100644 index 0000000000..5117238c6c --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomPresenter.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import kotlinx.coroutines.launch +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class JoinRoomPresenter @AssistedInject constructor( + @Assisted private val roomId: RoomId, + private val matrixClient: MatrixClient, + private val roomListService: RoomListService, +) : Presenter { + + interface Factory { + fun create(roomId: RoomId): JoinRoomPresenter + } + + @Composable + override fun present(): JoinRoomState { + val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty()) + val joinAuthorisationStatus = joinAuthorisationStatus(userMembership) + val roomInfo by produceState>(initialValue = AsyncData.Uninitialized, key1 = userMembership) { + when { + userMembership.isPresent -> { + val roomInfo = matrixClient.getRoom(roomId)?.let { + RoomInfo( + roomId = it.roomId, + roomName = it.displayName, + roomAlias = it.alias, + memberCount = it.activeMemberCount, + roomAvatarUrl = it.avatarUrl + ) + } + value = roomInfo?.let { AsyncData.Success(it) } ?: AsyncData.Failure(Exception("Failed to load room info")) + } + else -> { + value = AsyncData.Uninitialized + } + } + } + + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: JoinRoomEvents) { + when (event) { + JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> { + coroutineScope.launch { + matrixClient.joinRoom(roomId) + } + } + JoinRoomEvents.DeclineInvite -> { + coroutineScope.launch { + matrixClient.getRoom(roomId)?.use { + it.leave() + } + } + } + } + } + + return JoinRoomState( + roomInfo = roomInfo, + joinAuthorisationStatus = joinAuthorisationStatus, + currentAction = CurrentAction.None, + eventSink = ::handleEvents + ) + } + + @Composable + private fun joinAuthorisationStatus(userMembership: Optional): JoinAuthorisationStatus { + return when { + userMembership.getOrNull() == CurrentUserMembership.INVITED -> return JoinAuthorisationStatus.IsInvited + else -> JoinAuthorisationStatus.Unknown + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt new file mode 100644 index 0000000000..481fb13642 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomState.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.architecture.AsyncData +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 + +@Immutable +data class JoinRoomState( + val roomInfo: AsyncData, + val joinAuthorisationStatus: JoinAuthorisationStatus, + val currentAction: CurrentAction, + val eventSink: (JoinRoomEvents) -> Unit +) + +data class RoomInfo( + val roomId: RoomId, + val roomName: String, + val roomAlias: String?, + val memberCount: Long?, + val roomAvatarUrl: String?, +) { + fun avatarData(size: AvatarSize): AvatarData { + return AvatarData( + id = roomId.value, + name = roomName, + url = roomAvatarUrl, + size = size, + ) + } +} + +enum class JoinAuthorisationStatus { + IsInvited, + CanKnock, + CanJoin, + Unknown, +} + +sealed interface CurrentAction { + data object None : CurrentAction +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt new file mode 100644 index 0000000000..928edb4ce4 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomStateProvider.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.RoomId + +open class JoinRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aJoinRoomState( + roomInfo = AsyncData.Uninitialized + ), + aJoinRoomState( + joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin + ), + aJoinRoomState( + joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock + ), + aJoinRoomState( + joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited + ), + ) +} + +fun aJoinRoomState( + roomInfo: AsyncData = AsyncData.Success( + RoomInfo( + roomId = RoomId("@exa:matrix.org"), + roomName = "Element x android", + roomAlias = "#exa:matrix.org", + memberCount = null, + roomAvatarUrl = null + ) + ), + joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown, + currentAction: CurrentAction = CurrentAction.None, + eventSink: (JoinRoomEvents) -> Unit = {} +) = JoinRoomState( + roomInfo = roomInfo, + joinAuthorisationStatus = joinAuthorisationStatus, + currentAction = currentAction, + eventSink = eventSink +) + diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt new file mode 100644 index 0000000000..9fcda27e25 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/JoinRoomView.kt @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +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 +import io.element.android.compound.theme.ElementTheme +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 +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun JoinRoomView( + state: JoinRoomState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + HeaderFooterPage( + modifier = modifier, + topBar = { + JoinRoomTopBar(asyncRoomInfo = state.roomInfo, onBackClicked = onBackPressed) + }, + content = { + JoinRoomContent(state = state) + }, + footer = { + JoinRoomFooter( + joinAuthorisationStatus = state.joinAuthorisationStatus, + onAcceptInvite = { + state.eventSink(JoinRoomEvents.AcceptInvite) + }, + onDeclineInvite = { + state.eventSink(JoinRoomEvents.DeclineInvite) + }, + onJoinRoom = { + state.eventSink(JoinRoomEvents.JoinRoom) + }, + ) + } + ) +} + +@Composable +private fun JoinRoomFooter( + joinAuthorisationStatus: JoinAuthorisationStatus, + onAcceptInvite: () -> Unit, + onDeclineInvite: () -> Unit, + onJoinRoom: () -> Unit, + modifier: Modifier = Modifier, +) { + when (joinAuthorisationStatus) { + JoinAuthorisationStatus.IsInvited -> { + ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) { + OutlinedButton( + text = stringResource(CommonStrings.action_decline), + onClick = onDeclineInvite, + modifier = Modifier.weight(1f), + size = ButtonSize.Medium, + ) + Button( + text = stringResource(CommonStrings.action_accept), + onClick = onAcceptInvite, + modifier = Modifier.weight(1f), + size = ButtonSize.Medium, + ) + } + } + // TODO handle all cases properly + else -> { + Button( + text = stringResource(CommonStrings.action_join), + onClick = onJoinRoom, + modifier = modifier.fillMaxWidth(), + size = ButtonSize.Medium, + ) + } + } +} + +@Composable +private fun JoinRoomContent( + state: JoinRoomState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(80.dp)) + 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) + } + } + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Preview is not available", + style = ElementTheme.typography.fontHeadingMdBold, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textPrimary, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "You must be a member of this room to view the message history.", + style = ElementTheme.typography.fontBodyMdRegular, + textAlign = TextAlign.Center, + color = ElementTheme.colors.textSecondary, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun JoinRoomTopBar( + asyncRoomInfo: AsyncData, + onBackClicked: () -> Unit, +) { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClicked) + }, + title = { + when (asyncRoomInfo) { + is AsyncData.Success -> { + val roomInfo = asyncRoomInfo.data + 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 { + JoinRoomView( + state = state, + onBackPressed = { } + ) +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt new file mode 100644 index 0000000000..fa7635a312 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/join/di/JoinRoomModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.room.join.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.appnav.room.join.JoinRoomPresenter +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 + +@Module +@ContributesTo(SessionScope::class) +object JoinRoomModule { + @Provides + fun providesJoinRoomPresenterFactory( + roomListService: RoomListService, + client: MatrixClient, + ): JoinRoomPresenter.Factory { + return object : JoinRoomPresenter.Factory { + override fun create(roomId: RoomId): JoinRoomPresenter { + return JoinRoomPresenter( + roomId = roomId, + matrixClient = client, + roomListService = roomListService + ) + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt index 9388b54880..f6f5c3cb81 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -29,11 +30,14 @@ import io.element.android.libraries.designsystem.theme.components.TextButton @Composable fun ButtonRowMolecule( modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween, + verticalAlignment: Alignment.Vertical = Alignment.Top, content: @Composable RowScope.() -> Unit ) { Row( modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, ) { content() }