Room selection: add a limitation of 10 rooms for Share and Forward actions.

This commit is contained in:
Benoit Marty
2026-06-12 09:22:29 +02:00
parent c0c2f59822
commit d33852d32f
11 changed files with 75 additions and 17 deletions
@@ -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,
)
}
@@ -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,
)
}
@@ -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<RoomId>)
fun onCancel()
}
companion object {
const val DEFAULT_MAX_NUMBER_OF_ROOMS = 10
}
}
@@ -26,7 +26,10 @@ class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint {
return parentNode.createNode<RoomSelectNode>(
buildContext = buildContext,
plugins = listOf(
RoomSelectNode.Inputs(mode = params.mode),
RoomSelectNode.Inputs(
mode = params.mode,
maxNumberOfRooms = params.maxNumberOfRooms,
),
callback,
)
)
@@ -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()
@@ -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<RoomSelectState> {
@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,
@@ -16,9 +16,12 @@ import kotlinx.collections.immutable.ImmutableList
data class RoomSelectState(
val mode: RoomSelectMode,
val maxNumberOfRooms: Int,
val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
val searchQuery: TextFieldState,
val isSearchActive: Boolean,
val selectedRooms: ImmutableList<SelectRoomInfo>,
val eventSink: (RoomSelectEvent) -> Unit
)
val eventSink: (RoomSelectEvent) -> Unit,
) {
val canSelectMoreRooms = selectedRooms.size < maxNumberOfRooms
}
@@ -40,11 +40,18 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
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<ImmutableList<SelectRoomInfo>> = 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,
@@ -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) },
)
}
}
@@ -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<RoomId>) = 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)
}
}
@@ -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(