Merge pull request #1975 from element-hq/feature/bma/extractForward
Extract RoomList select to its own module
This commit is contained in:
commit
baa3bfc0d1
45 changed files with 1011 additions and 468 deletions
|
|
@ -58,6 +58,7 @@ dependencies {
|
|||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.roomselect.api)
|
||||
implementation(projects.libraries.voicerecorder.api)
|
||||
implementation(projects.libraries.mediaplayer.api)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
|
|
|
|||
|
|
@ -16,14 +16,6 @@
|
|||
|
||||
package io.element.android.features.messages.impl.forward
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
|
||||
sealed interface ForwardMessagesEvents {
|
||||
data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents
|
||||
// TODO remove to restore multi-selection
|
||||
data object RemoveSelectedRoom : ForwardMessagesEvents
|
||||
data object ToggleSearchActive : ForwardMessagesEvents
|
||||
data class UpdateQuery(val query: String) : ForwardMessagesEvents
|
||||
data object ForwardEvent : ForwardMessagesEvents
|
||||
data object ClearError : ForwardMessagesEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,15 @@
|
|||
|
||||
package io.element.android.features.messages.impl.forward
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -29,14 +34,28 @@ import io.element.android.libraries.architecture.inputs
|
|||
import io.element.android.libraries.di.RoomScope
|
||||
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)
|
||||
class ForwardMessagesNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: ForwardMessagesPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||
) : ParentNode<ForwardMessagesNode.NavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(NavTarget),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
|
||||
@Parcelize
|
||||
object NavTarget : Parcelable
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||
|
|
@ -48,6 +67,39 @@ class ForwardMessagesNode @AssistedInject constructor(
|
|||
private val presenter = presenterFactory.create(inputs.eventId.value)
|
||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
val callback = object : RoomSelectEntryPoint.Callback {
|
||||
override fun onRoomSelected(roomIds: List<RoomId>) {
|
||||
presenter.onRoomSelected(roomIds)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
return roomSelectEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.params(RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward))
|
||||
.build()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
// Will render to room select screen
|
||||
Children(
|
||||
navModel = navModel,
|
||||
)
|
||||
|
||||
val state = presenter.present()
|
||||
ForwardMessagesView(
|
||||
state = state,
|
||||
onForwardingSucceeded = ::onSucceeded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSucceeded(roomIds: ImmutableList<RoomId>) {
|
||||
navigateUp()
|
||||
if (roomIds.size == 1) {
|
||||
|
|
@ -55,15 +107,4 @@ class ForwardMessagesNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.onForwardedToSingleRoom(targetRoomId) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
ForwardMessagesView(
|
||||
state = state,
|
||||
onDismiss = ::navigateUp,
|
||||
onForwardingSucceeded = ::onSucceeded,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,28 +17,20 @@
|
|||
package io.element.android.features.messages.impl.forward
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
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
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -47,7 +39,6 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
|||
@Assisted eventId: String,
|
||||
private val room: MatrixRoom,
|
||||
private val matrixCoroutineScope: CoroutineScope,
|
||||
private val client: MatrixClient,
|
||||
) : Presenter<ForwardMessagesState> {
|
||||
|
||||
private val eventId: EventId = EventId(eventId)
|
||||
|
|
@ -57,62 +48,25 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
|||
fun create(eventId: String): ForwardMessagesPresenter
|
||||
}
|
||||
|
||||
private val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = mutableStateOf(Async.Uninitialized)
|
||||
|
||||
fun onRoomSelected(roomIds: List<RoomId>) {
|
||||
matrixCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): ForwardMessagesState {
|
||||
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
||||
var query by remember { mutableStateOf<String>("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
|
||||
val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
|
||||
val summaries by client.roomListService.allRooms.summaries.collectAsState()
|
||||
|
||||
LaunchedEffect(query, summaries) {
|
||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
||||
.map { it.details }
|
||||
.filter { it.name.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.NoResults()
|
||||
}
|
||||
}
|
||||
|
||||
val forwardingSucceeded by remember {
|
||||
derivedStateOf { forwardingActionState.value.dataOrNull() }
|
||||
}
|
||||
|
||||
fun handleEvents(event: ForwardMessagesEvents) {
|
||||
when (event) {
|
||||
is ForwardMessagesEvents.SetSelectedRoom -> {
|
||||
selectedRooms = persistentListOf(event.room)
|
||||
// Restore for multi-selection
|
||||
// val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId }
|
||||
// selectedRooms = if (index >= 0) {
|
||||
// selectedRooms.removeAt(index)
|
||||
// } else {
|
||||
// selectedRooms.add(event.room)
|
||||
// }
|
||||
}
|
||||
ForwardMessagesEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
||||
is ForwardMessagesEvents.UpdateQuery -> query = event.query
|
||||
ForwardMessagesEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||
ForwardMessagesEvents.ForwardEvent -> {
|
||||
isSearchActive = false
|
||||
val roomIds = selectedRooms.map { it.roomId }.toPersistentList()
|
||||
matrixCoroutineScope.forwardEvent(eventId, roomIds, forwardingActionState)
|
||||
}
|
||||
ForwardMessagesEvents.ClearError -> forwardingActionState.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
return ForwardMessagesState(
|
||||
resultState = results,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
selectedRooms = selectedRooms,
|
||||
isForwarding = forwardingActionState.value.isLoading(),
|
||||
error = (forwardingActionState.value as? Async.Failure)?.error,
|
||||
forwardingSucceeded = forwardingSucceeded,
|
||||
|
|
|
|||
|
|
@ -16,16 +16,11 @@
|
|||
|
||||
package io.element.android.features.messages.impl.forward
|
||||
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ForwardMessagesState(
|
||||
val resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>>,
|
||||
val query: String,
|
||||
val isSearchActive: Boolean,
|
||||
val selectedRooms: ImmutableList<RoomSummaryDetails>,
|
||||
// TODO Migrate to an Async
|
||||
val isForwarding: Boolean,
|
||||
val error: Throwable?,
|
||||
val forwardingSucceeded: ImmutableList<RoomId>?,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.features.messages.impl.forward
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
|
|
@ -29,38 +28,13 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessag
|
|||
override val values: Sequence<ForwardMessagesState>
|
||||
get() = sequenceOf(
|
||||
aForwardMessagesState(),
|
||||
aForwardMessagesState(query = "Test", isSearchActive = true),
|
||||
aForwardMessagesState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())),
|
||||
aForwardMessagesState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
),
|
||||
aForwardMessagesState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain")))
|
||||
),
|
||||
aForwardMessagesState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))),
|
||||
isForwarding = true,
|
||||
),
|
||||
aForwardMessagesState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))),
|
||||
forwardingSucceeded = persistentListOf(RoomId("!room2:domain")),
|
||||
),
|
||||
aForwardMessagesState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))),
|
||||
error = Throwable("error"),
|
||||
),
|
||||
// Add other states here
|
||||
|
|
@ -68,18 +42,10 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessag
|
|||
}
|
||||
|
||||
fun aForwardMessagesState(
|
||||
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.NotSearching(),
|
||||
query: String = "",
|
||||
isSearchActive: Boolean = false,
|
||||
selectedRooms: ImmutableList<RoomSummaryDetails> = persistentListOf(),
|
||||
isForwarding: Boolean = false,
|
||||
error: Throwable? = null,
|
||||
forwardingSucceeded: ImmutableList<RoomId>? = null,
|
||||
) = ForwardMessagesState(
|
||||
resultState = resultState,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
selectedRooms = selectedRooms,
|
||||
isForwarding = isForwarding,
|
||||
error = error,
|
||||
forwardingSucceeded = forwardingSucceeded,
|
||||
|
|
|
|||
|
|
@ -16,63 +16,20 @@
|
|||
|
||||
package io.element.android.features.messages.impl.forward
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.TextOverflow
|
||||
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.designsystem.components.ProgressDialog
|
||||
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.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ForwardMessagesView(
|
||||
state: ForwardMessagesState,
|
||||
onDismiss: () -> Unit,
|
||||
onForwardingSucceeded: (ImmutableList<RoomId>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -81,193 +38,16 @@ fun ForwardMessagesView(
|
|||
return
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) {
|
||||
// TODO toggle selection when multi-selection is enabled
|
||||
state.eventSink(ForwardMessagesEvents.RemoveSelectedRoom)
|
||||
if (state.isForwarding) {
|
||||
ProgressDialog(modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummaryDetails>) {
|
||||
if (isForwarding) return
|
||||
SelectedRooms(
|
||||
selectedRooms = selectedRooms,
|
||||
onRoomRemoved = ::onRoomRemoved,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
if (state.error != null) {
|
||||
ForwardingErrorDialog(
|
||||
modifier = modifier,
|
||||
onDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) },
|
||||
)
|
||||
}
|
||||
|
||||
fun onBackButton(state: ForwardMessagesState) {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(ForwardMessagesEvents.ToggleSearchActive)
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(onBack = { onBackButton(state) })
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_forward_message),
|
||||
style = ElementTheme.typography.aliasScreenTitle
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { onBackButton(state) })
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_send),
|
||||
enabled = state.selectedRooms.isNotEmpty(),
|
||||
onClick = { state.eventSink(ForwardMessagesEvents.ForwardEvent) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
SearchBar(
|
||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||
query = state.query,
|
||||
onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) },
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(ForwardMessagesEvents.ToggleSearchActive) },
|
||||
resultState = state.resultState,
|
||||
showBackButton = false,
|
||||
) { summaries ->
|
||||
LazyColumn {
|
||||
item {
|
||||
SelectedRoomsHelper(
|
||||
isForwarding = state.isForwarding,
|
||||
selectedRooms = state.selectedRooms
|
||||
)
|
||||
}
|
||||
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
||||
Column {
|
||||
RoomSummaryView(
|
||||
roomSummary,
|
||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||
onSelection = { roomSummary ->
|
||||
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
|
||||
}
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
// TODO restore for multi-selection
|
||||
// SelectedRoomsHelper(
|
||||
// isForwarding = state.isForwarding,
|
||||
// selectedRooms = state.selectedRooms
|
||||
// )
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
if (state.resultState is SearchBarResultState.Results) {
|
||||
LazyColumn {
|
||||
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
||||
Column {
|
||||
RoomSummaryView(
|
||||
roomSummary,
|
||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||
onSelection = { roomSummary ->
|
||||
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
|
||||
}
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isForwarding) {
|
||||
ProgressDialog()
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
ForwardingErrorDialog(onDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedRooms(
|
||||
selectedRooms: ImmutableList<RoomSummaryDetails>,
|
||||
onRoomRemoved: (RoomSummaryDetails) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
items(selectedRooms, key = { it.roomId.value }) { roomSummary ->
|
||||
SelectedRoom(roomSummary = roomSummary, onRoomRemoved = onRoomRemoved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomSummaryView(
|
||||
summary: RoomSummaryDetails,
|
||||
isSelected: Boolean,
|
||||
onSelection: (RoomSummaryDetails) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable { onSelection(summary) }
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.heightIn(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = roomAlias,
|
||||
name = summary.name,
|
||||
url = summary.avatarURLString,
|
||||
size = AvatarSize.ForwardRoomListItem,
|
||||
),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
text = summary.name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Id
|
||||
Text(
|
||||
text = roomAlias,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
RadioButton(selected = isSelected, onClick = { onSelection(summary) })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -284,7 +64,6 @@ private fun ForwardingErrorDialog(onDismiss: () -> Unit, modifier: Modifier = Mo
|
|||
internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = ElementPreview {
|
||||
ForwardMessagesView(
|
||||
state = state,
|
||||
onDismiss = {},
|
||||
onForwardingSucceeded = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,16 +20,11 @@ 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.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -40,7 +35,6 @@ class ForwardMessagesPresenterTests {
|
|||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
|
|
@ -48,75 +42,23 @@ class ForwardMessagesPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedRooms).isEmpty()
|
||||
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.isForwarding).isFalse()
|
||||
assertThat(initialState.error).isNull()
|
||||
assertThat(initialState.forwardingSucceeded).isNull()
|
||||
|
||||
// Search is run automatically
|
||||
val searchState = awaitItem()
|
||||
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - toggle search active`() = runTest {
|
||||
fun `present - forward successful`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive)
|
||||
assertThat(awaitItem().isSearchActive).isTrue()
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive)
|
||||
assertThat(awaitItem().isSearchActive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - update query`() = runTest {
|
||||
val roomListService = FakeRoomListService().apply {
|
||||
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
|
||||
}
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val presenter = aPresenter(client = client)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetail())))
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.UpdateQuery("string not contained"))
|
||||
assertThat(awaitItem().query).isEqualTo("string not contained")
|
||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - select a room and forward successful`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary))
|
||||
awaitItem()
|
||||
|
||||
// Test successful forwarding
|
||||
initialState.eventSink(ForwardMessagesEvents.ForwardEvent)
|
||||
|
||||
presenter.onRoomSelected(listOf(summary.roomId))
|
||||
val forwardingState = awaitItem()
|
||||
assertThat(forwardingState.isSearchActive).isFalse()
|
||||
assertThat(forwardingState.isForwarding).isTrue()
|
||||
|
||||
val successfulForwardState = awaitItem()
|
||||
assertThat(successfulForwardState.isForwarding).isFalse()
|
||||
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
|
||||
|
|
@ -130,43 +72,17 @@ class ForwardMessagesPresenterTests {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary))
|
||||
awaitItem()
|
||||
|
||||
// Test failed forwarding
|
||||
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
||||
initialState.eventSink(ForwardMessagesEvents.ForwardEvent)
|
||||
skipItems(1)
|
||||
|
||||
val failedForwardState = awaitItem()
|
||||
assertThat(failedForwardState.isForwarding).isFalse()
|
||||
assertThat(failedForwardState.error).isNotNull()
|
||||
|
||||
// Then clear error
|
||||
initialState.eventSink(ForwardMessagesEvents.ClearError)
|
||||
assertThat(awaitItem().error).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - select and remove a room`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary))
|
||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
||||
|
||||
initialState.eventSink(ForwardMessagesEvents.RemoveSelectedRoom)
|
||||
assertThat(awaitItem().selectedRooms).isEmpty()
|
||||
presenter.onRoomSelected(listOf(summary.roomId))
|
||||
skipItems(1)
|
||||
val failedForwardState = awaitItem()
|
||||
assertThat(failedForwardState.error).isNotNull()
|
||||
// Then clear error
|
||||
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
|
||||
assertThat(awaitItem().error).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +90,9 @@ class ForwardMessagesPresenterTests {
|
|||
eventId: EventId = AN_EVENT_ID,
|
||||
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
||||
coroutineScope: CoroutineScope = this,
|
||||
client: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client)
|
||||
) = ForwardMessagesPresenter(
|
||||
eventId = eventId.value,
|
||||
room = fakeMatrixRoom,
|
||||
matrixCoroutineScope = coroutineScope,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
27
libraries/roomselect/api/build.gradle.kts
Normal file
27
libraries/roomselect/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.roomselect.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface RoomSelectEntryPoint : FeatureEntryPoint {
|
||||
data class Params(
|
||||
val mode: RoomSelectMode,
|
||||
)
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
interface NodeBuilder {
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onRoomSelected(roomIds: List<RoomId>)
|
||||
fun onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.api
|
||||
|
||||
enum class RoomSelectMode {
|
||||
Forward,
|
||||
}
|
||||
52
libraries/roomselect/impl/build.gradle.kts
Normal file
52
libraries/roomselect/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.roomselect.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.roomselect.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultRoomSelectEntryPoint @Inject constructor() : RoomSelectEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : RoomSelectEntryPoint.NodeBuilder {
|
||||
override fun params(params: RoomSelectEntryPoint.Params): RoomSelectEntryPoint.NodeBuilder {
|
||||
plugins += RoomSelectNode.Inputs(mode = params.mode)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun callback(callback: RoomSelectEntryPoint.Callback): RoomSelectEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<RoomSelectNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.matrix.api.roomlist.RoomSummaryDetails
|
||||
|
||||
sealed interface RoomSelectEvents {
|
||||
data class SetSelectedRoom(val room: RoomSummaryDetails) : RoomSelectEvents
|
||||
|
||||
// TODO remove to restore multi-selection
|
||||
data object RemoveSelectedRoom : RoomSelectEvents
|
||||
data object ToggleSearchActive : RoomSelectEvents
|
||||
data class UpdateQuery(val query: String) : RoomSelectEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 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
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class RoomSelectNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: RoomSelectPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
data class Inputs(
|
||||
val mode: RoomSelectMode,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val presenter = presenterFactory.create(inputs.mode)
|
||||
|
||||
private val callbacks = plugins.filterIsInstance<RoomSelectEntryPoint.Callback>()
|
||||
|
||||
private fun onDismiss() {
|
||||
callbacks.forEach { it.onCancel() }
|
||||
}
|
||||
|
||||
private fun onSubmit(roomIds: List<RoomId>) {
|
||||
callbacks.forEach { it.onRoomSelected(roomIds) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomSelectView(
|
||||
state = state,
|
||||
onDismiss = ::onDismiss,
|
||||
onSubmit = ::onSubmit,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class RoomSelectPresenter @AssistedInject constructor(
|
||||
@Assisted private val mode: RoomSelectMode,
|
||||
private val client: MatrixClient,
|
||||
) : Presenter<RoomSelectState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(mode: RoomSelectMode): RoomSelectPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomSelectState {
|
||||
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
||||
var query by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
|
||||
|
||||
val summaries by client.roomListService.allRooms.summaries.collectAsState()
|
||||
|
||||
LaunchedEffect(query, summaries) {
|
||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
||||
.map { it.details }
|
||||
.filter { it.name.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.NoResults()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: RoomSelectEvents) {
|
||||
when (event) {
|
||||
is RoomSelectEvents.SetSelectedRoom -> {
|
||||
selectedRooms = persistentListOf(event.room)
|
||||
// Restore for multi-selection
|
||||
// val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId }
|
||||
// selectedRooms = if (index >= 0) {
|
||||
// selectedRooms.removeAt(index)
|
||||
// } else {
|
||||
// selectedRooms.add(event.room)
|
||||
// }
|
||||
}
|
||||
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
||||
is RoomSelectEvents.UpdateQuery -> query = event.query
|
||||
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||
}
|
||||
}
|
||||
|
||||
return RoomSelectState(
|
||||
mode = mode,
|
||||
resultState = results,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
selectedRooms = selectedRooms,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.roomselect.api.RoomSelectMode
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomSelectState(
|
||||
val mode: RoomSelectMode,
|
||||
val resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>>,
|
||||
val query: String,
|
||||
val isSearchActive: Boolean,
|
||||
val selectedRooms: ImmutableList<RoomSummaryDetails>,
|
||||
val eventSink: (RoomSelectEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
||||
override val values: Sequence<RoomSelectState>
|
||||
get() = sequenceOf(
|
||||
aRoomSelectState(),
|
||||
aRoomSelectState(query = "Test", isSearchActive = true),
|
||||
aRoomSelectState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())),
|
||||
aRoomSelectState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
),
|
||||
aRoomSelectState(
|
||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain")))
|
||||
),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomSelectState(
|
||||
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.NotSearching(),
|
||||
query: String = "",
|
||||
isSearchActive: Boolean = false,
|
||||
selectedRooms: ImmutableList<RoomSummaryDetails> = persistentListOf(),
|
||||
) = RoomSelectState(
|
||||
mode = RoomSelectMode.Forward,
|
||||
resultState = resultState,
|
||||
query = query,
|
||||
isSearchActive = isSearchActive,
|
||||
selectedRooms = selectedRooms,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
internal fun aForwardMessagesRoomList() = persistentListOf(
|
||||
aRoomDetailsState(),
|
||||
aRoomDetailsState(roomId = RoomId("!room2:domain"), canonicalAlias = "#element-x-room:matrix.org"),
|
||||
)
|
||||
|
||||
fun aRoomDetailsState(
|
||||
roomId: RoomId = RoomId("!room:domain"),
|
||||
name: String = "roomName",
|
||||
canonicalAlias: String? = null,
|
||||
isDirect: Boolean = true,
|
||||
avatarURLString: String? = null,
|
||||
lastMessage: RoomMessage? = null,
|
||||
lastMessageTimestamp: Long? = null,
|
||||
unreadNotificationCount: Int = 0,
|
||||
inviter: RoomMember? = null,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = canonicalAlias,
|
||||
isDirect = isDirect,
|
||||
avatarURLString = avatarURLString,
|
||||
lastMessage = lastMessage,
|
||||
lastMessageTimestamp = lastMessageTimestamp,
|
||||
unreadNotificationCount = unreadNotificationCount,
|
||||
inviter = inviter,
|
||||
)
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.TextOverflow
|
||||
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.roomselect.api.RoomSelectMode
|
||||
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.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomSelectView(
|
||||
state: RoomSelectState,
|
||||
onDismiss: () -> Unit,
|
||||
onSubmit: (List<RoomId>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) {
|
||||
// TODO toggle selection when multi-selection is enabled
|
||||
state.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummaryDetails>) {
|
||||
if (isForwarding) return
|
||||
SelectedRooms(
|
||||
selectedRooms = selectedRooms,
|
||||
onRoomRemoved = ::onRoomRemoved,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
fun onBackButton(state: RoomSelectState) {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(onBack = { onBackButton(state) })
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = when (state.mode) {
|
||||
RoomSelectMode.Forward -> stringResource(CommonStrings.common_forward_message)
|
||||
},
|
||||
style = ElementTheme.typography.aliasScreenTitle
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { onBackButton(state) })
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_send),
|
||||
enabled = state.selectedRooms.isNotEmpty(),
|
||||
onClick = { onSubmit(state.selectedRooms.map { it.roomId }) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
SearchBar(
|
||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||
query = state.query,
|
||||
onQueryChange = { state.eventSink(RoomSelectEvents.UpdateQuery(it)) },
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(RoomSelectEvents.ToggleSearchActive) },
|
||||
resultState = state.resultState,
|
||||
showBackButton = false,
|
||||
) { summaries ->
|
||||
LazyColumn {
|
||||
item {
|
||||
SelectedRoomsHelper(
|
||||
isForwarding = false, // TODO state.isForwarding,
|
||||
selectedRooms = state.selectedRooms
|
||||
)
|
||||
}
|
||||
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
||||
Column {
|
||||
RoomSummaryView(
|
||||
roomSummary,
|
||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||
onSelection = { roomSummary ->
|
||||
state.eventSink(RoomSelectEvents.SetSelectedRoom(roomSummary))
|
||||
}
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
// TODO restore for multi-selection
|
||||
// SelectedRoomsHelper(
|
||||
// isForwarding = state.isForwarding,
|
||||
// selectedRooms = state.selectedRooms
|
||||
// )
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
if (state.resultState is SearchBarResultState.Results) {
|
||||
LazyColumn {
|
||||
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
||||
Column {
|
||||
RoomSummaryView(
|
||||
roomSummary,
|
||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||
onSelection = { roomSummary ->
|
||||
state.eventSink(RoomSelectEvents.SetSelectedRoom(roomSummary))
|
||||
}
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedRooms(
|
||||
selectedRooms: ImmutableList<RoomSummaryDetails>,
|
||||
onRoomRemoved: (RoomSummaryDetails) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
items(selectedRooms, key = { it.roomId.value }) { roomSummary ->
|
||||
SelectedRoom(roomSummary = roomSummary, onRoomRemoved = onRoomRemoved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomSummaryView(
|
||||
summary: RoomSummaryDetails,
|
||||
isSelected: Boolean,
|
||||
onSelection: (RoomSummaryDetails) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable { onSelection(summary) }
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.heightIn(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = roomAlias,
|
||||
name = summary.name,
|
||||
url = summary.avatarURLString,
|
||||
size = AvatarSize.ForwardRoomListItem,
|
||||
),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
text = summary.name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Id
|
||||
Text(
|
||||
text = roomAlias,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
RadioButton(selected = isSelected, onClick = { onSelection(summary) })
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomSelectViewPreview(@PreviewParameter(RoomSelectStateProvider::class) state: RoomSelectState) = ElementPreview {
|
||||
RoomSelectView(
|
||||
state = state,
|
||||
onDismiss = {},
|
||||
onSubmit = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 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.roomselect.api.RoomSelectMode
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
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.aRoomSummaryDetail
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class RoomSelectPresenterTests {
|
||||
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedRooms).isEmpty()
|
||||
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
// Search is run automatically
|
||||
val searchState = awaitItem()
|
||||
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - toggle search active`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - update query`() = runTest {
|
||||
val roomListService = FakeRoomListService().apply {
|
||||
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
|
||||
}
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val presenter = aPresenter(client = client)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetail())))
|
||||
|
||||
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
|
||||
assertThat(awaitItem().query).isEqualTo("string not contained")
|
||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - select and remove a room`() = runTest {
|
||||
val presenter = aPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
|
||||
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
|
||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
||||
|
||||
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||
assertThat(awaitItem().selectedRooms).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun aPresenter(
|
||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||
client: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = RoomSelectPresenter(
|
||||
mode = mode,
|
||||
client = client,
|
||||
)
|
||||
}
|
||||
|
|
@ -102,6 +102,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
|||
implementation(project(":libraries:mediaupload:impl"))
|
||||
implementation(project(":libraries:usersearch:impl"))
|
||||
implementation(project(":libraries:textcomposer:impl"))
|
||||
implementation(project(":libraries:roomselect:impl"))
|
||||
implementation(project(":libraries:cryptography:impl"))
|
||||
implementation(project(":libraries:voicerecorder:impl"))
|
||||
implementation(project(":libraries:mediaplayer:impl"))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05fae38cdda7b3e00a355ecade04c54b5fa2921a529aa51af7cf90122caa9769
|
||||
size 14972
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83af8ee61dc0ddf6fdbbe58d49266d78d3a0809c57b91ea5995bd1c47b1c00f9
|
||||
size 12648
|
||||
oid sha256:cf70dc030f96b929ba3817694a624de715b82719117fc75720c4e3ff8a8188c8
|
||||
size 9962
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a4f6f8527d28a0c1c8cf0520b8927ae0e6f8adcaf0d56d0a117d48700dee89b7
|
||||
size 28388
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6349c7d27b2c227801212825e8f162da43f9b466ffb746b0f24b520a1fcdd146
|
||||
size 26298
|
||||
oid sha256:a8e4490d507c06fae92e6f05aed7c31ae5a20e2a52bc23efc7db7d6e1290dc36
|
||||
size 10807
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0cd4916c256811a70c4ee39409f48328920a663524d65922adeeb518dba3b59
|
||||
size 28225
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e9ea671e95d6f6526c67e9a6048a1e9e91de1f42cd91a1b4a76138c8b08fe65
|
||||
size 24884
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:764689e0dc5b509a5c36493ab7454ff5dc0c269fab6c68c57cbd89198b254506
|
||||
size 13853
|
||||
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||
size 4464
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:323b97bd6852a87e23ebb29f06a8406931de786d0bc702cd464718ec8ba7f8e4
|
||||
size 11799
|
||||
oid sha256:a8b6b2f2e2c03e7927ccb93d4fa8e914f7a3a19f4526cda8237e324dda3d9d3f
|
||||
size 7548
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e3d8829874805bdcfcb0b0978d9decbb463ec6d31f0d5cf5406dde0f1f786df9
|
||||
size 27104
|
||||
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||
size 4464
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29fb9eae961e82ca62e29ac5894dcdaac5290f69195cbb899e4862f522e1592c
|
||||
size 25484
|
||||
oid sha256:a90e9ecf5cc6c042d9a72401b378ae0de46244803a9729fe339df7b7b676e603
|
||||
size 8225
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9fdef27a785ede0a878d6f59ff27843cb836361b3cbdf045f107d7980ea38bc
|
||||
size 25759
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||
size 4464
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c98f350f39909171eb7b852efd43f434e7aaee31a8262ec079ba12879906077d
|
||||
size 21721
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05fae38cdda7b3e00a355ecade04c54b5fa2921a529aa51af7cf90122caa9769
|
||||
size 14972
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83af8ee61dc0ddf6fdbbe58d49266d78d3a0809c57b91ea5995bd1c47b1c00f9
|
||||
size 12648
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a4f6f8527d28a0c1c8cf0520b8927ae0e6f8adcaf0d56d0a117d48700dee89b7
|
||||
size 28388
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6349c7d27b2c227801212825e8f162da43f9b466ffb746b0f24b520a1fcdd146
|
||||
size 26298
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:764689e0dc5b509a5c36493ab7454ff5dc0c269fab6c68c57cbd89198b254506
|
||||
size 13853
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:323b97bd6852a87e23ebb29f06a8406931de786d0bc702cd464718ec8ba7f8e4
|
||||
size 11799
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e3d8829874805bdcfcb0b0978d9decbb463ec6d31f0d5cf5406dde0f1f786df9
|
||||
size 27104
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29fb9eae961e82ca62e29ac5894dcdaac5290f69195cbb899e4862f522e1592c
|
||||
size 25484
|
||||
Loading…
Add table
Add a link
Reference in a new issue