Merge pull request #211 from vector-im/feature/fre/start_chat_search_matrixid
[Start chat] Show a single result when searching for a matrixId
This commit is contained in:
commit
02f2ace1e4
26 changed files with 246 additions and 64 deletions
|
|
@ -16,7 +16,12 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
sealed interface CreateRoomRootEvents {
|
||||
data class UpdateSearchQuery(val query: String) : CreateRoomRootEvents
|
||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||
object CreateRoom : CreateRoomRootEvents
|
||||
object InvitePeople : CreateRoomRootEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : CreateRoomRootEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,23 +17,76 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateRoomRootPresenter @Inject constructor() : Presenter<CreateRoomRootState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): CreateRoomRootState {
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
val searchResults: MutableState<ImmutableList<MatrixUser>> = remember {
|
||||
mutableStateOf(persistentListOf())
|
||||
}
|
||||
|
||||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
is CreateRoomRootEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is CreateRoomRootEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is CreateRoomRootEvents.StartDM -> handleStartDM(event.matrixUser)
|
||||
CreateRoomRootEvents.CreateRoom -> Unit // Todo Handle create room action
|
||||
CreateRoomRootEvents.InvitePeople -> Unit // Todo Handle invite people action
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
||||
searchResults.value = if (MatrixPatterns.isUserId(searchQuery)) {
|
||||
persistentListOf(MatrixUser(UserId(searchQuery)))
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
// Perform the search asynchronously
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchResults.value = performSearch(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
return CreateRoomRootState(
|
||||
eventSink = ::handleEvents
|
||||
eventSink = ::handleEvents,
|
||||
isSearchActive = isSearchActive,
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun performSearch(query: String): ImmutableList<MatrixUser> {
|
||||
val isMatrixId = MatrixPatterns.isUserId(query)
|
||||
val results = mutableListOf<MatrixUser>()// TODO trigger /search request
|
||||
if (isMatrixId && results.none { it.id.value == query }) {
|
||||
val getProfileResult: MatrixUser? = null // TODO trigger /profile request
|
||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||
results.add(0, profile)
|
||||
}
|
||||
return results.toImmutableList()
|
||||
}
|
||||
|
||||
private fun handleStartDM(matrixUser: MatrixUser) {
|
||||
Timber.d("handleStartDM: $matrixUser") // Todo handle start DM action
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,13 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
// TODO add your ui models. Remove the eventSink if you don't have events.
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
// Do not use default value, so no member get forgotten in the presenters.
|
||||
data class CreateRoomRootState(
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
val isSearchActive: Boolean,
|
||||
val searchQuery: String,
|
||||
val searchResults: ImmutableList<MatrixUser>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,15 +17,43 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
||||
override val values: Sequence<CreateRoomRootState>
|
||||
get() = sequenceOf(
|
||||
aCreateRoomRootState(),
|
||||
// Add other state here
|
||||
aCreateRoomRootState().copy(isSearchActive = true),
|
||||
aCreateRoomRootState().copy(isSearchActive = true, searchQuery = "someone"),
|
||||
aCreateRoomRootState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
searchResults = persistentListOf(
|
||||
MatrixUser(id = UserId("@someone:matrix.org")),
|
||||
MatrixUser(id = UserId("@someone:matrix.org"), username = "someone"),
|
||||
MatrixUser(
|
||||
id = UserId("@someone_with_a_very_long_matrix_identifier:a_very_long_domain.org"),
|
||||
username = "hey, I am someone with a very long display name"
|
||||
),
|
||||
MatrixUser(id = UserId("@someone_2:matrix.org"), username = "someone 2"),
|
||||
MatrixUser(id = UserId("@someone_3:matrix.org"), username = "someone 3"),
|
||||
MatrixUser(id = UserId("@someone_4:matrix.org"), username = "someone 4"),
|
||||
MatrixUser(id = UserId("@someone_5:matrix.org"), username = "someone 5"),
|
||||
MatrixUser(id = UserId("@someone_6:matrix.org"), username = "someone 6"),
|
||||
MatrixUser(id = UserId("@someone_7:matrix.org"), username = "someone 7"),
|
||||
MatrixUser(id = UserId("@someone_8:matrix.org"), username = "someone 8"),
|
||||
MatrixUser(id = UserId("@someone_9:matrix.org"), username = "someone 9"),
|
||||
MatrixUser(id = UserId("@someone_10:matrix.org"), username = "someone 10"),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aCreateRoomRootState() = CreateRoomRootState(
|
||||
eventSink = {}
|
||||
eventSink = {},
|
||||
isSearchActive = false,
|
||||
searchQuery = "",
|
||||
searchResults = persistentListOf(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,18 +22,16 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
|
@ -45,15 +43,19 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.DockedSearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
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.Text
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.designsystem.R as DrawableR
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
|
|
@ -64,12 +66,10 @@ fun CreateRoomRootView(
|
|||
modifier: Modifier = Modifier,
|
||||
onClosePressed: () -> Unit = {},
|
||||
) {
|
||||
var searchText by rememberSaveable { mutableStateOf("") }
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
topBar = {
|
||||
if (!isSearchActive) {
|
||||
if (!state.isSearchActive) {
|
||||
CreateRoomRootViewTopBar(onClosePressed = onClosePressed)
|
||||
}
|
||||
}
|
||||
|
|
@ -80,14 +80,16 @@ fun CreateRoomRootView(
|
|||
) {
|
||||
CreateRoomSearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = searchText,
|
||||
query = state.searchQuery,
|
||||
placeHolderTitle = stringResource(StringR.string.search_for_someone),
|
||||
active = isSearchActive,
|
||||
onActiveChanged = { isSearchActive = it },
|
||||
onTextChanged = { searchText = it },
|
||||
results = state.searchResults,
|
||||
active = state.isSearchActive,
|
||||
onActiveChanged = { state.eventSink(CreateRoomRootEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChanged = { state.eventSink(CreateRoomRootEvents.UpdateSearchQuery(it)) },
|
||||
onResultSelected = { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
|
||||
)
|
||||
|
||||
if (!isSearchActive) {
|
||||
if (!state.isSearchActive) {
|
||||
CreateRoomActionButtonsList(
|
||||
onNewRoomClicked = { state.eventSink(CreateRoomRootEvents.CreateRoom) },
|
||||
onInvitePeopleClicked = { state.eventSink(CreateRoomRootEvents.InvitePeople) },
|
||||
|
|
@ -123,12 +125,14 @@ fun CreateRoomRootViewTopBar(
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreateRoomSearchBar(
|
||||
text: String,
|
||||
query: String,
|
||||
placeHolderTitle: String,
|
||||
results: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onActiveChanged: (Boolean) -> Unit = {},
|
||||
onTextChanged: (String) -> Unit = {},
|
||||
onResultSelected: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
|
@ -137,8 +141,8 @@ fun CreateRoomSearchBar(
|
|||
focusManager.clearFocus()
|
||||
}
|
||||
|
||||
DockedSearchBar(
|
||||
query = text,
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = onTextChanged,
|
||||
onSearch = { focusManager.clearFocus() },
|
||||
active = active,
|
||||
|
|
@ -153,9 +157,11 @@ fun CreateRoomSearchBar(
|
|||
},
|
||||
leadingIcon = if (active) {
|
||||
{ BackButton(onClick = { onActiveChanged(false) }) }
|
||||
} else null,
|
||||
} else {
|
||||
null
|
||||
},
|
||||
trailingIcon = when {
|
||||
active && text.isNotEmpty() -> {
|
||||
active && query.isNotEmpty() -> {
|
||||
{
|
||||
IconButton(onClick = { onTextChanged("") }) {
|
||||
Icon(Icons.Default.Close, stringResource(StringR.string.a11y_clear))
|
||||
|
|
@ -173,9 +179,17 @@ fun CreateRoomSearchBar(
|
|||
}
|
||||
else -> null
|
||||
},
|
||||
shape = if (!active) SearchBarDefaults.dockedShape else SearchBarDefaults.fullScreenShape,
|
||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||
content = {},
|
||||
content = {
|
||||
LazyColumn {
|
||||
items(results) {
|
||||
CreateRoomSearchResultItem(
|
||||
matrixUser = it,
|
||||
onClick = { onResultSelected(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +213,20 @@ fun CreateRoomActionButtonsList(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateRoomSearchResultItem(
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
MatrixUserRow(
|
||||
modifier = modifier,
|
||||
matrixUser = matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateRoomActionButton(
|
||||
@DrawableRes iconRes: Int,
|
||||
|
|
@ -209,7 +237,7 @@ fun CreateRoomActionButton(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 56.dp)
|
||||
.height(56.dp)
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.impl.root.CreateRoomRootEvents
|
||||
import io.element.android.features.createroom.impl.root.CreateRoomRootPresenter
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -42,7 +42,7 @@ class CreateRoomRootPresenterTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - send event`() = runTest {
|
||||
fun `present - trigger action buttons`() = runTest {
|
||||
val presenter = CreateRoomRootPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -52,4 +52,42 @@ class CreateRoomRootPresenterTests {
|
|||
initialState.eventSink(CreateRoomRootEvents.InvitePeople) // Not implemented yet
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - update search query`() = runTest {
|
||||
val presenter = CreateRoomRootPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(CreateRoomRootEvents.OnSearchActiveChanged(true))
|
||||
assertThat(awaitItem().isSearchActive).isTrue()
|
||||
|
||||
val matrixIdQuery = "@name:matrix.org"
|
||||
initialState.eventSink(CreateRoomRootEvents.UpdateSearchQuery(matrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery)))
|
||||
|
||||
val notMatrixIdQuery = "name"
|
||||
initialState.eventSink(CreateRoomRootEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).isEmpty()
|
||||
|
||||
initialState.eventSink(CreateRoomRootEvents.OnSearchActiveChanged(false))
|
||||
assertThat(awaitItem().isSearchActive).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - trigger start DM action`() = runTest {
|
||||
val presenter = CreateRoomRootPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val matrixUser = MatrixUser(UserId("@name:matrix.org"))
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue