Browse Source

Merge pull request #2954 from element-hq/feature/bma/messageForwardFix

Message forward fix
pull/2961/head
Benoit Marty 4 months ago committed by GitHub
parent
commit
d89004f174
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt
  2. 33
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
  3. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt
  4. 23
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
  5. 40
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt
  6. 22
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt
  7. 80
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt
  8. 14
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenterTest.kt
  9. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
  10. 4
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt
  11. 6
      libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTest.kt
  12. 44
      libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt
  13. 74
      libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt
  14. 51
      libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt
  15. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_ForwardMessagesView_null_ForwardMessagesView-Day-2_2_null_3,NEXUS_5,1.0,en].png
  16. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_ForwardMessagesView_null_ForwardMessagesView-Night-2_3_null_3,NEXUS_5,1.0,en].png

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt

@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.core.EventId @@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@ -99,7 +98,7 @@ class ForwardMessagesNode @AssistedInject constructor( @@ -99,7 +98,7 @@ class ForwardMessagesNode @AssistedInject constructor(
}
}
private fun onForwardSuccess(roomIds: ImmutableList<RoomId>) {
private fun onForwardSuccess(roomIds: List<RoomId>) {
navigateUp()
if (roomIds.size == 1) {
val targetRoomId = roomIds.first()

33
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt

@ -18,15 +18,13 @@ package io.element.android.features.messages.impl.forward @@ -18,15 +18,13 @@ package io.element.android.features.messages.impl.forward
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
@ -38,7 +36,7 @@ import kotlinx.coroutines.launch @@ -38,7 +36,7 @@ import kotlinx.coroutines.launch
class ForwardMessagesPresenter @AssistedInject constructor(
@Assisted eventId: String,
private val matrixCoroutineScope: CoroutineScope,
private val appCoroutineScope: CoroutineScope,
private val timelineProvider: TimelineProvider,
) : Presenter<ForwardMessagesState> {
private val eventId: EventId = EventId(eventId)
@ -48,28 +46,22 @@ class ForwardMessagesPresenter @AssistedInject constructor( @@ -48,28 +46,22 @@ class ForwardMessagesPresenter @AssistedInject constructor(
fun create(eventId: String): ForwardMessagesPresenter
}
private val forwardingActionState: MutableState<AsyncData<ImmutableList<RoomId>>> = mutableStateOf(AsyncData.Uninitialized)
private val forwardingActionState: MutableState<AsyncAction<List<RoomId>>> = mutableStateOf(AsyncAction.Uninitialized)
fun onRoomSelected(roomIds: List<RoomId>) {
matrixCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
appCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
}
@Composable
override fun present(): ForwardMessagesState {
val forwardingSucceeded by remember {
derivedStateOf { forwardingActionState.value.dataOrNull() }
}
fun handleEvents(event: ForwardMessagesEvents) {
when (event) {
ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncData.Uninitialized
ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
}
}
return ForwardMessagesState(
isForwarding = forwardingActionState.value.isLoading(),
error = (forwardingActionState.value as? AsyncData.Failure)?.error,
forwardingSucceeded = forwardingSucceeded,
forwardAction = forwardingActionState.value,
eventSink = { handleEvents(it) }
)
}
@ -77,12 +69,11 @@ class ForwardMessagesPresenter @AssistedInject constructor( @@ -77,12 +69,11 @@ class ForwardMessagesPresenter @AssistedInject constructor(
private fun CoroutineScope.forwardEvent(
eventId: EventId,
roomIds: ImmutableList<RoomId>,
isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>,
isForwardMessagesState: MutableState<AsyncAction<List<RoomId>>>,
) = launch {
isForwardMessagesState.value = AsyncData.Loading()
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).fold(
{ isForwardMessagesState.value = AsyncData.Success(roomIds) },
{ isForwardMessagesState.value = AsyncData.Failure(it) }
)
suspend {
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).getOrThrow()
roomIds
}.runCatchingUpdatingState(isForwardMessagesState)
}
}

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt

@ -16,13 +16,10 @@ @@ -16,13 +16,10 @@
package io.element.android.features.messages.impl.forward
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
data class ForwardMessagesState(
// TODO Migrate to an Async
val isForwarding: Boolean,
val error: Throwable?,
val forwardingSucceeded: ImmutableList<RoomId>?,
val forwardAction: AsyncAction<List<RoomId>>,
val eventSink: (ForwardMessagesEvents) -> Unit
)

23
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt

@ -17,34 +17,31 @@ @@ -17,34 +17,31 @@
package io.element.android.features.messages.impl.forward
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessagesState> {
override val values: Sequence<ForwardMessagesState>
get() = sequenceOf(
aForwardMessagesState(),
aForwardMessagesState(
isForwarding = true,
forwardAction = AsyncAction.Loading,
),
aForwardMessagesState(
forwardingSucceeded = persistentListOf(RoomId("!room2:domain")),
forwardAction = AsyncAction.Success(
listOf(RoomId("!room2:domain")),
)
),
aForwardMessagesState(
error = Throwable("error"),
forwardAction = AsyncAction.Failure(Throwable("error")),
),
// Add other states here
)
}
fun aForwardMessagesState(
isForwarding: Boolean = false,
error: Throwable? = null,
forwardingSucceeded: ImmutableList<RoomId>? = null,
forwardAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
eventSink: (ForwardMessagesEvents) -> Unit = {}
) = ForwardMessagesState(
isForwarding = isForwarding,
error = error,
forwardingSucceeded = forwardingSucceeded,
eventSink = {}
forwardAction = forwardAction,
eventSink = eventSink
)

40
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt

@ -17,45 +17,25 @@ @@ -17,45 +17,25 @@
package io.element.android.features.messages.impl.forward
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
@Composable
fun ForwardMessagesView(
state: ForwardMessagesState,
onForwardSuccess: (ImmutableList<RoomId>) -> Unit,
modifier: Modifier = Modifier,
onForwardSuccess: (List<RoomId>) -> Unit,
) {
if (state.forwardingSucceeded != null) {
onForwardSuccess(state.forwardingSucceeded)
return
}
if (state.isForwarding) {
ProgressDialog(modifier)
}
if (state.error != null) {
ForwardingErrorDialog(
modifier = modifier,
onDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) },
)
}
}
@Composable
private fun ForwardingErrorDialog(onDismiss: () -> Unit, modifier: Modifier = Modifier) {
ErrorDialog(
content = ErrorDialogDefaults.title,
onDismiss = onDismiss,
modifier = modifier,
AsyncActionView(
async = state.forwardAction,
onSuccess = {
onForwardSuccess(it)
},
onErrorDismiss = {
state.eventSink(ForwardMessagesEvents.ClearError)
},
)
}

22
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTest.kt

@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode @@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.AN_EVENT_ID
@ -28,13 +29,11 @@ import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails @@ -28,13 +29,11 @@ import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.lang.IllegalStateException
class ForwardMessagesPresenterTest {
@get:Rule
@ -47,9 +46,7 @@ class ForwardMessagesPresenterTest { @@ -47,9 +46,7 @@ class ForwardMessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isForwarding).isFalse()
assertThat(initialState.error).isNull()
assertThat(initialState.forwardingSucceeded).isNull()
assertThat(initialState.forwardAction.isUninitialized()).isTrue()
}
}
@ -70,11 +67,10 @@ class ForwardMessagesPresenterTest { @@ -70,11 +67,10 @@ class ForwardMessagesPresenterTest {
val summary = aRoomSummaryDetails()
presenter.onRoomSelected(listOf(summary.roomId))
val forwardingState = awaitItem()
assertThat(forwardingState.isForwarding).isTrue()
assertThat(forwardingState.forwardAction.isLoading()).isTrue()
val successfulForwardState = awaitItem()
assertThat(successfulForwardState.isForwarding).isFalse()
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
assert(forwardEventLambda).isCalledOnce()
assertThat(successfulForwardState.forwardAction).isEqualTo(AsyncAction.Success(listOf(summary.roomId)))
forwardEventLambda.assertions().isCalledOnce()
}
}
@ -96,11 +92,11 @@ class ForwardMessagesPresenterTest { @@ -96,11 +92,11 @@ class ForwardMessagesPresenterTest {
presenter.onRoomSelected(listOf(summary.roomId))
skipItems(1)
val failedForwardState = awaitItem()
assertThat(failedForwardState.error).isNotNull()
assertThat(failedForwardState.forwardAction.isFailure()).isTrue()
// Then clear error
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
assertThat(awaitItem().error).isNull()
assert(forwardEventLambda).isCalledOnce()
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
forwardEventLambda.assertions().isCalledOnce()
}
}
@ -111,6 +107,6 @@ class ForwardMessagesPresenterTest { @@ -111,6 +107,6 @@ class ForwardMessagesPresenterTest {
) = ForwardMessagesPresenter(
eventId = eventId.value,
timelineProvider = LiveTimelineProvider(fakeMatrixRoom),
matrixCoroutineScope = coroutineScope,
appCoroutineScope = coroutineScope,
)
}

80
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesViewTest.kt

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* 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.features.messages.impl.forward
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.testtags.TestTags
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ForwardMessagesViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `cancel error emits the expected event`() {
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>()
rule.setForwardMessagesView(
aForwardMessagesState(
forwardAction = AsyncAction.Failure(AN_EXCEPTION),
eventSink = eventsRecorder
),
)
rule.pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError)
}
@Test
fun `success invokes onForwardSuccess`() {
val data = listOf(A_ROOM_ID)
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>(expectEvents = false)
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
rule.setForwardMessagesView(
aForwardMessagesState(
forwardAction = AsyncAction.Success(data),
eventSink = eventsRecorder
),
onForwardSuccess = callback,
)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setForwardMessagesView(
state: ForwardMessagesState,
onForwardSuccess: (List<RoomId>) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
ForwardMessagesView(
state = state,
onForwardSuccess = onForwardSuccess,
)
}
}

14
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenterTest.kt

@ -37,7 +37,7 @@ class ReportMessagePresenterTest { @@ -37,7 +37,7 @@ class ReportMessagePresenterTest {
@Test
fun `presenter - initial state`() = runTest {
val presenter = aPresenter()
val presenter = createReportMessagePresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -50,7 +50,7 @@ class ReportMessagePresenterTest { @@ -50,7 +50,7 @@ class ReportMessagePresenterTest {
@Test
fun `presenter - update reason`() = runTest {
val presenter = aPresenter()
val presenter = createReportMessagePresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -64,7 +64,7 @@ class ReportMessagePresenterTest { @@ -64,7 +64,7 @@ class ReportMessagePresenterTest {
@Test
fun `presenter - toggle block user`() = runTest {
val presenter = aPresenter()
val presenter = createReportMessagePresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -82,7 +82,7 @@ class ReportMessagePresenterTest { @@ -82,7 +82,7 @@ class ReportMessagePresenterTest {
@Test
fun `presenter - handle successful report and block user`() = runTest {
val room = FakeMatrixRoom()
val presenter = aPresenter(matrixRoom = room)
val presenter = createReportMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -99,7 +99,7 @@ class ReportMessagePresenterTest { @@ -99,7 +99,7 @@ class ReportMessagePresenterTest {
@Test
fun `presenter - handle successful report`() = runTest {
val room = FakeMatrixRoom()
val presenter = aPresenter(matrixRoom = room)
val presenter = createReportMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -116,7 +116,7 @@ class ReportMessagePresenterTest { @@ -116,7 +116,7 @@ class ReportMessagePresenterTest {
val room = FakeMatrixRoom().apply {
givenReportContentResult(Result.failure(Exception("Failed to report content")))
}
val presenter = aPresenter(matrixRoom = room)
val presenter = createReportMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -132,7 +132,7 @@ class ReportMessagePresenterTest { @@ -132,7 +132,7 @@ class ReportMessagePresenterTest {
}
}
private fun aPresenter(
private fun createReportMessagePresenter(
inputs: ReportMessagePresenter.Inputs = ReportMessagePresenter.Inputs(AN_EVENT_ID, A_USER_ID),
matrixRoom: MatrixRoom = FakeMatrixRoom(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt

@ -20,6 +20,7 @@ sealed interface RoomListFilter { @@ -20,6 +20,7 @@ sealed interface RoomListFilter {
companion object {
/**
* Create a filter that matches all the given filters.
* If no filters are provided, all the rooms will match.
*/
fun all(vararg filters: RoomListFilter): RoomListFilter {
return All(filters.toList())
@ -35,6 +36,7 @@ sealed interface RoomListFilter { @@ -35,6 +36,7 @@ sealed interface RoomListFilter {
/**
* A filter that matches all the given filters.
* If [filters] is empty, all the room will match.
*/
data class All(
val filters: List<RoomListFilter>

4
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt

@ -54,8 +54,8 @@ interface RoomListService { @@ -54,8 +54,8 @@ interface RoomListService {
): DynamicRoomList
/**
* returns a [DynamicRoomList] object of all rooms we want to display.
* This will exclude some rooms like the invites, or spaces.
* Returns a [DynamicRoomList] object of all rooms we want to display.
* If you want to get a filtered room list, consider using [createRoomList].
*/
val allRooms: DynamicRoomList

6
libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTest.kt

@ -136,4 +136,10 @@ class RoomListFilterTest { @@ -136,4 +136,10 @@ class RoomListFilterTest {
)
assertThat(roomSummaries.filter(filter)).isEmpty()
}
@Test
fun `Room list filter all with empty list`() = runTest {
val filter = RoomListFilter.all()
assertThat(roomSummaries.filter(filter)).isEqualTo(roomSummaries)
}
}

44
libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt

@ -19,6 +19,7 @@ package io.element.android.libraries.roomselect.impl @@ -19,6 +19,7 @@ package io.element.android.libraries.roomselect.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -28,17 +29,14 @@ import dagger.assisted.AssistedFactory @@ -28,17 +29,14 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toImmutableList
class RoomSelectPresenter @AssistedInject constructor(
@Assisted private val mode: RoomSelectMode,
private val client: MatrixClient,
private val dataSource: RoomSelectSearchDataSource,
) : Presenter<RoomSelectState> {
@AssistedFactory
interface Factory {
@ -48,22 +46,26 @@ class RoomSelectPresenter @AssistedInject constructor( @@ -48,22 +46,26 @@ class RoomSelectPresenter @AssistedInject constructor(
@Composable
override fun present(): RoomSelectState {
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
var query by remember { mutableStateOf("") }
var searchQuery by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) }
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.Initial()) }
val summaries by client.roomListService.allRooms.summaries.collectAsState(initial = emptyList())
LaunchedEffect(Unit) {
dataSource.load()
}
LaunchedEffect(searchQuery) {
dataSource.setSearchQuery(searchQuery)
}
LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
.map { it.details }
.filter { it.name.orEmpty().contains(query, ignoreCase = true) }
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
.toPersistentList()
results = if (filteredSummaries.isNotEmpty()) {
SearchBarResultState.Results(filteredSummaries)
} else {
SearchBarResultState.NoResultsFound()
val roomSummaryDetailsList by dataSource.roomSummaries.collectAsState(initial = persistentListOf())
val searchResults by remember {
derivedStateOf {
when {
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
isSearchActive -> SearchBarResultState.NoResultsFound()
else -> SearchBarResultState.Initial()
}
}
}
@ -80,15 +82,15 @@ class RoomSelectPresenter @AssistedInject constructor( @@ -80,15 +82,15 @@ class RoomSelectPresenter @AssistedInject constructor(
// }
}
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
is RoomSelectEvents.UpdateQuery -> query = event.query
is RoomSelectEvents.UpdateQuery -> searchQuery = event.query
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
}
}
return RoomSelectState(
mode = mode,
resultState = results,
query = query,
resultState = searchResults,
query = searchQuery,
isSearchActive = isSearchActive,
selectedRooms = selectedRooms,
eventSink = { handleEvents(it) }

74
libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectSearchDataSource.kt

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
/*
* 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.libraries.roomselect.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private const val PAGE_SIZE = 30
/**
* DataSource for RoomSummaryDetails that can be filtered by a search query,
* and which only includes rooms the user has joined.
*/
class RoomSelectSearchDataSource @Inject constructor(
roomListService: RoomListService,
coroutineDispatchers: CoroutineDispatchers,
) {
private val roomList = roomListService.createRoomList(
pageSize = PAGE_SIZE,
initialFilter = RoomListFilter.all(),
source = RoomList.Source.All,
)
val roomSummaries: Flow<PersistentList<RoomSummaryDetails>> = roomList.filteredSummaries
.map { roomSummaries ->
roomSummaries
.filterIsInstance<RoomSummary.Filled>()
.map { it.details }
.filter { it.currentUserMembership == CurrentUserMembership.JOINED }
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
.toPersistentList()
}
.flowOn(coroutineDispatchers.computation)
suspend fun load() = coroutineScope {
roomList.loadAllIncrementally(this)
}
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
val filter = if (searchQuery.isBlank()) {
RoomListFilter.all()
} else {
RoomListFilter.NormalizedMatchRoomName(searchQuery)
}
roomList.updateFilter(filter)
}
}

51
libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt

@ -21,13 +21,16 @@ import app.cash.molecule.moleculeFlow @@ -21,13 +21,16 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -38,7 +41,7 @@ class RoomSelectPresenterTest { @@ -38,7 +41,7 @@ class RoomSelectPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = aPresenter()
val presenter = createRoomSelectPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -46,24 +49,18 @@ class RoomSelectPresenterTest { @@ -46,24 +49,18 @@ class RoomSelectPresenterTest {
assertThat(initialState.selectedRooms).isEmpty()
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(initialState.isSearchActive).isFalse()
// Search is run automatically
val searchState = awaitItem()
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
}
}
@Test
fun `present - toggle search active`() = runTest {
val presenter = aPresenter()
val presenter = createRoomSelectPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
assertThat(awaitItem().isSearchActive).isTrue()
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
assertThat(awaitItem().isSearchActive).isFalse()
}
@ -74,43 +71,59 @@ class RoomSelectPresenterTest { @@ -74,43 +71,59 @@ class RoomSelectPresenterTest {
val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails())))
}
val client = FakeMatrixClient(roomListService = roomListService)
val presenter = aPresenter(client = client)
val presenter = createRoomSelectPresenter(
roomListService = roomListService
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetails())))
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
skipItems(1)
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
assertThat(
roomListService.allRooms.currentFilter.value
).isEqualTo(
RoomListFilter.NormalizedMatchRoomName("string not contained")
)
assertThat(awaitItem().query).isEqualTo("string not contained")
roomListService.postAllRooms(
emptyList()
)
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
}
}
@Test
fun `present - select and remove a room`() = runTest {
val presenter = aPresenter()
val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails())))
}
val presenter = createRoomSelectPresenter(
roomListService = roomListService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
skipItems(1)
val summary = aRoomSummaryDetails()
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
assertThat(awaitItem().selectedRooms).isEmpty()
cancel()
}
}
private fun aPresenter(
private fun TestScope.createRoomSelectPresenter(
mode: RoomSelectMode = RoomSelectMode.Forward,
client: FakeMatrixClient = FakeMatrixClient(),
roomListService: RoomListService = FakeRoomListService(),
) = RoomSelectPresenter(
mode = mode,
client = client,
dataSource = RoomSelectSearchDataSource(
roomListService = roomListService,
coroutineDispatchers = testCoroutineDispatchers(),
),
)
}

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_ForwardMessagesView_null_ForwardMessagesView-Day-2_2_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_ForwardMessagesView_null_ForwardMessagesView-Night-2_3_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save