From d33852d32fe3aead2ad7bac21e351442c564c15c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 12 Jun 2026 09:22:29 +0200 Subject: [PATCH] Room selection: add a limitation of 10 rooms for Share and Forward actions. --- .../forward/impl/ForwardMessagesNode.kt | 5 ++++- .../android/features/share/impl/ShareNode.kt | 5 ++++- .../roomselect/api/RoomSelectEntryPoint.kt | 5 +++++ .../impl/DefaultRoomSelectEntryPoint.kt | 5 ++++- .../roomselect/impl/RoomSelectNode.kt | 6 +++++- .../roomselect/impl/RoomSelectPresenter.kt | 7 ++++++- .../roomselect/impl/RoomSelectState.kt | 7 +++++-- .../roomselect/impl/RoomSelectStateProvider.kt | 8 ++++++++ .../roomselect/impl/RoomSelectView.kt | 13 ++++++++++--- .../impl/DefaultRoomSelectEntryPointTest.kt | 13 +++++++++---- .../roomselect/impl/RoomSelectPresenterTest.kt | 18 +++++++++++++++--- 11 files changed, 75 insertions(+), 17 deletions(-) diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt index eb085d19c5..1c565a390a 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt @@ -74,7 +74,10 @@ class ForwardMessagesNode( return roomSelectEntryPoint.createNode( parentNode = this, buildContext = buildContext, - params = RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward), + params = RoomSelectEntryPoint.Params( + mode = RoomSelectMode.Forward, + maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS, + ), callback = callback, ) } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt index 0597b3b678..1abac1afa6 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt @@ -70,7 +70,10 @@ class ShareNode( return roomSelectEntryPoint.createNode( parentNode = this, buildContext = buildContext, - params = RoomSelectEntryPoint.Params(mode = RoomSelectMode.Share), + params = RoomSelectEntryPoint.Params( + mode = RoomSelectMode.Share, + maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS, + ), callback = callback, ) } diff --git a/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt b/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt index ba5ca79d40..3c5c5d5ac3 100644 --- a/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt +++ b/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.core.RoomId interface RoomSelectEntryPoint : FeatureEntryPoint { data class Params( val mode: RoomSelectMode, + val maxNumberOfRooms: Int, ) fun createNode( @@ -30,4 +31,8 @@ interface RoomSelectEntryPoint : FeatureEntryPoint { fun onRoomSelected(roomIds: List) fun onCancel() } + + companion object { + const val DEFAULT_MAX_NUMBER_OF_ROOMS = 10 + } } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt index 4ca92663a9..ff3ba06f0d 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt @@ -26,7 +26,10 @@ class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint { return parentNode.createNode( buildContext = buildContext, plugins = listOf( - RoomSelectNode.Inputs(mode = params.mode), + RoomSelectNode.Inputs( + mode = params.mode, + maxNumberOfRooms = params.maxNumberOfRooms, + ), callback, ) ) diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt index 943da643c6..0297f13066 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt @@ -35,10 +35,14 @@ class RoomSelectNode( ) : Node(buildContext, plugins = plugins) { data class Inputs( val mode: RoomSelectMode, + val maxNumberOfRooms: Int, ) : NodeInputs private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.mode) + private val presenter = presenterFactory.create( + mode = inputs.mode, + maxNumberOfRooms = inputs.maxNumberOfRooms, + ) private val stateFlow = launchMolecule { presenter.present() } private val callback: RoomSelectEntryPoint.Callback = callback() diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index 76488ea18e..e73299126e 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -34,11 +34,15 @@ import kotlinx.coroutines.launch @AssistedInject class RoomSelectPresenter( @Assisted private val mode: RoomSelectMode, + @Assisted private val maxNumberOfRooms: Int, private val dataSourceFactory: RoomSelectSearchDataSource.Factory, ) : Presenter { @AssistedFactory fun interface Factory { - fun create(mode: RoomSelectMode): RoomSelectPresenter + fun create( + mode: RoomSelectMode, + maxNumberOfRooms: Int, + ): RoomSelectPresenter } @Composable @@ -86,6 +90,7 @@ class RoomSelectPresenter( return RoomSelectState( mode = mode, + maxNumberOfRooms = maxNumberOfRooms, resultState = searchResults, searchQuery = queryState, isSearchActive = isSearchActive, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt index d89dab7d72..aadb7c10d1 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt @@ -16,9 +16,12 @@ import kotlinx.collections.immutable.ImmutableList data class RoomSelectState( val mode: RoomSelectMode, + val maxNumberOfRooms: Int, val resultState: SearchBarResultState>, val searchQuery: TextFieldState, val isSearchActive: Boolean, val selectedRooms: ImmutableList, - val eventSink: (RoomSelectEvent) -> Unit -) + val eventSink: (RoomSelectEvent) -> Unit, +) { + val canSelectMoreRooms = selectedRooms.size < maxNumberOfRooms +} diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt index 1f8c10ec15..131e35429e 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt @@ -40,11 +40,18 @@ open class RoomSelectStateProvider : PreviewParameterProvider { mode = RoomSelectMode.Share, resultState = SearchBarResultState.Results(aRoomSelectRoomList()), ), + aRoomSelectState( + mode = RoomSelectMode.Share, + resultState = SearchBarResultState.Results(aRoomSelectRoomList()), + selectedRooms = aRoomSelectRoomList().subList(0, 1), + maxNumberOfRooms = 1, + ), ) } internal fun aRoomSelectState( mode: RoomSelectMode = RoomSelectMode.Forward, + maxNumberOfRooms: Int = 10, resultState: SearchBarResultState> = SearchBarResultState.Initial(), searchQuery: String = "", isSearchActive: Boolean = false, @@ -52,6 +59,7 @@ internal fun aRoomSelectState( eventSink: (RoomSelectEvent) -> Unit = {}, ) = RoomSelectState( mode = mode, + maxNumberOfRooms = maxNumberOfRooms, resultState = resultState, searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 0618430ba4..10d1212644 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -155,7 +155,8 @@ fun RoomSelectView( isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, onSelection = { roomSummary -> state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary)) - } + }, + canBeSelected = state.canSelectMoreRooms, ) HorizontalDivider(modifier = Modifier.fillMaxWidth()) } @@ -178,7 +179,8 @@ fun RoomSelectView( isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, onSelection = { roomSummary -> state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary)) - } + }, + canBeSelected = state.canSelectMoreRooms, ) HorizontalDivider(modifier = Modifier.fillMaxWidth()) } @@ -211,6 +213,7 @@ private fun SelectedRooms( private fun RoomSummaryView( roomInfo: SelectRoomInfo, isSelected: Boolean, + canBeSelected: Boolean, onSelection: (SelectRoomInfo) -> Unit, ) { Row( @@ -255,7 +258,11 @@ private fun RoomSummaryView( ) } } - RadioButton(selected = isSelected, onClick = { onSelection(roomInfo) }) + RadioButton( + selected = isSelected, + enabled = isSelected || canBeSelected, + onClick = { onSelection(roomInfo) }, + ) } } diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt index a99a0396c2..ab9f0b2663 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt @@ -33,13 +33,15 @@ class DefaultRoomSelectEntryPointTest { fun `test node builder`() = runTest { val entryPoint = DefaultRoomSelectEntryPoint() val testMode = RoomSelectMode.Share + val testMaxNumberOfRooms = 12 val parentNode = TestParentNode.create { buildContext, plugins -> RoomSelectNode( buildContext = buildContext, plugins = plugins, - presenterFactory = { mode -> + presenterFactory = { mode, maxNumberOfRooms -> assertThat(mode).isEqualTo(testMode) - createRoomSelectPresenter(mode) + assertThat(maxNumberOfRooms).isEqualTo(testMaxNumberOfRooms) + createRoomSelectPresenter(mode, maxNumberOfRooms) }, ) } @@ -47,7 +49,10 @@ class DefaultRoomSelectEntryPointTest { override fun onRoomSelected(roomIds: List) = lambdaError() override fun onCancel() = lambdaError() } - val params = RoomSelectEntryPoint.Params(testMode) + val params = RoomSelectEntryPoint.Params( + mode = testMode, + maxNumberOfRooms = testMaxNumberOfRooms, + ) val result = entryPoint.createNode( parentNode = parentNode, buildContext = BuildContext.root(null), @@ -55,7 +60,7 @@ class DefaultRoomSelectEntryPointTest { callback = callback, ) assertThat(result).isInstanceOf(RoomSelectNode::class.java) - assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode)) + assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode, params.maxNumberOfRooms)) assertThat(result.plugins).contains(callback) } } diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 3542bddb32..b5a4ccfd12 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo +import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.assert @@ -43,6 +44,8 @@ class RoomSelectPresenterTest { assertThat(initialState.selectedRooms).isEmpty() assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.isSearchActive).isFalse() + assertThat(initialState.maxNumberOfRooms).isEqualTo(10) + assertThat(initialState.canSelectMoreRooms).isTrue() } } @@ -102,15 +105,22 @@ class RoomSelectPresenterTest { createRoomListLambda = { roomList } ) val presenter = createRoomSelectPresenter( + maxNumberOfRooms = 1, roomListService = roomListService, ) presenter.test { val initialState = awaitItem() val roomInfo = roomSummary.toSelectRoomInfo() initialState.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomInfo)) - assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(roomInfo)) - initialState.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomInfo)) - assertThat(awaitItem().selectedRooms).isEmpty() + awaitItem().let { + assertThat(it.selectedRooms).isEqualTo(persistentListOf(roomInfo)) + assertThat(it.canSelectMoreRooms).isFalse() + it.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomInfo)) + } + awaitItem().let { + assertThat(it.selectedRooms).isEmpty() + assertThat(it.canSelectMoreRooms).isTrue() + } cancel() } } @@ -145,9 +155,11 @@ class RoomSelectPresenterTest { internal fun TestScope.createRoomSelectPresenter( mode: RoomSelectMode = RoomSelectMode.Forward, + maxNumberOfRooms: Int = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS, roomListService: RoomListService = FakeRoomListService(), ) = RoomSelectPresenter( mode = mode, + maxNumberOfRooms = maxNumberOfRooms, dataSourceFactory = object : RoomSelectSearchDataSource.Factory { override fun create(coroutineScope: CoroutineScope): RoomSelectSearchDataSource { return RoomSelectSearchDataSource(