Refactor search related functionality (#436)
Refactor search related functionality This is a prelude to adding the feature of inviting users to a room, getting everything in the right place and reusable. What this does: ## User search refactor Moves the (global) user search logic (dealing with MXIDs, minimum lengths, debounces) into a `UserRepository`. This now sits in a `usersearch` library, which will be used by the create room flow and the new invite flow. ## SearchBar logic pull-up Every place we use SearchBar, we're doing the same things to style placeholders, show back/cancel buttons, etc. We also have a results type that is duplicated for basically every feature that uses the search bar. I've pushed all this common functionality into the SearchBar itself. This makes the component a bit less general purpose, but saves a lot of repetition. ## Remove the userlist feature Almost all the functionality of the userlist feature is now exclusively used by the create room feature. Room details uses its own version because the requirements are different. Components useful elsewhere (SelectedUsers and SelectedUser) have gone to matrixui, everything else has gone to createroom. ## Other bits and pieces I've fixed everywhere that uses Scaffold to correctly consume the WindowInsets if the contentPadding is applied to the contents (which it universally is). This was a change in the last version of Material3 (I guess previously Scaffold handled the consumption for us). This fixes weird gaps above search bars. Added overloads for the MatrixUserRow and CheckedMatrixUserRow that take the name/subtitle/avatar separately, so the invites list can pass arbitrary text like "User has already been invited". The `blockuser` package was for some reason not under `impl` but alongside it, I've bumped it into the right place.
This commit is contained in:
parent
e2a2374c4a
commit
1eac67bf25
137 changed files with 1002 additions and 675 deletions
|
|
@ -45,10 +45,10 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.features.userlist.api)
|
|
||||||
implementation(projects.libraries.mediapickers.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
implementation(projects.libraries.usersearch.impl)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -59,10 +59,9 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.impl)
|
|
||||||
testImplementation(projects.features.userlist.test)
|
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.createroom.impl
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
||||||
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,17 @@ package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
class AddPeoplePresenter @Inject constructor(
|
class AddPeoplePresenter @Inject constructor(
|
||||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||||
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
private val userRepository: UserRepository,
|
||||||
private val dataStore: CreateRoomDataStore,
|
private val dataStore: CreateRoomDataStore,
|
||||||
) : Presenter<UserListState> {
|
) : Presenter<UserListState> {
|
||||||
|
|
||||||
|
|
@ -37,10 +36,8 @@ class AddPeoplePresenter @Inject constructor(
|
||||||
userListPresenterFactory.create(
|
userListPresenterFactory.create(
|
||||||
UserListPresenterArgs(
|
UserListPresenterArgs(
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
minimumSearchLength = 3,
|
|
||||||
searchDebouncePeriodMillis = UserListPresenterArgs.DEFAULT_DEBOUNCE
|
|
||||||
),
|
),
|
||||||
userListDataSource,
|
userRepository,
|
||||||
dataStore.selectedUserListDataStore,
|
dataStore.selectedUserListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
|
@ -30,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aUserListState(),
|
aUserListState(),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -30,8 +32,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.components.UserListView
|
||||||
import io.element.android.features.userlist.api.components.UserListView
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -41,6 +43,7 @@ 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.TextButton
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddPeopleView(
|
fun AddPeopleView(
|
||||||
state: UserListState,
|
state: UserListState,
|
||||||
|
|
@ -49,6 +52,7 @@ fun AddPeopleView(
|
||||||
onNextPressed: () -> Unit = {},
|
onNextPressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
AddPeopleViewTopBar(
|
AddPeopleViewTopBar(
|
||||||
|
|
@ -60,12 +64,14 @@ fun AddPeopleView(
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
) {
|
) {
|
||||||
UserListView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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.features.createroom.impl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchUserBar(
|
||||||
|
query: String,
|
||||||
|
state: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||||
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
|
active: Boolean,
|
||||||
|
isMultiSelectionEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
||||||
|
onActiveChanged: (Boolean) -> Unit = {},
|
||||||
|
onTextChanged: (String) -> Unit = {},
|
||||||
|
onUserSelected: (MatrixUser) -> Unit = {},
|
||||||
|
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onTextChanged,
|
||||||
|
active = active,
|
||||||
|
onActiveChange = onActiveChanged,
|
||||||
|
modifier = modifier,
|
||||||
|
placeHolderTitle = placeHolderTitle,
|
||||||
|
contentPrefix = {
|
||||||
|
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
||||||
|
SelectedUsersList(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
selectedUsers = selectedUsers,
|
||||||
|
autoScroll = true,
|
||||||
|
onUserRemoved = onUserDeselected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resultState = state,
|
||||||
|
resultHandler = { users ->
|
||||||
|
LazyColumn {
|
||||||
|
if (isMultiSelectionEnabled) {
|
||||||
|
items(users) { matrixUser ->
|
||||||
|
SearchMultipleUsersResultItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (checked) {
|
||||||
|
onUserSelected(matrixUser)
|
||||||
|
} else {
|
||||||
|
onUserDeselected(matrixUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(users) { matrixUser ->
|
||||||
|
SearchSingleUserResultItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
onClick = { onUserSelected(matrixUser) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -24,12 +24,13 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.userlist.api.UserListStateProvider
|
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserListView(
|
fun UserListView(
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,10 @@ import android.net.Uri
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
|
@ -48,7 +50,6 @@ import io.element.android.features.createroom.impl.components.Avatar
|
||||||
import io.element.android.features.createroom.impl.components.LabelledTextField
|
import io.element.android.features.createroom.impl.components.LabelledTextField
|
||||||
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
||||||
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListView
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListView
|
||||||
import io.element.android.features.userlist.api.components.SelectedUsersList
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
|
@ -60,10 +61,11 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.TextButton
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigureRoomView(
|
fun ConfigureRoomView(
|
||||||
state: ConfigureRoomState,
|
state: ConfigureRoomState,
|
||||||
|
|
@ -104,7 +106,9 @@ fun ConfigureRoomView(
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -21,25 +21,24 @@ import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.execute
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
class CreateRoomRootPresenter @Inject constructor(
|
class CreateRoomRootPresenter @Inject constructor(
|
||||||
private val presenterFactory: UserListPresenter.Factory,
|
private val presenterFactory: UserListPresenter.Factory,
|
||||||
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
private val userRepository: UserRepository,
|
||||||
private val userListDataStore: UserListDataStore,
|
private val userListDataStore: UserListDataStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
) : Presenter<CreateRoomRootState> {
|
) : Presenter<CreateRoomRootState> {
|
||||||
|
|
@ -48,10 +47,8 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
presenterFactory.create(
|
presenterFactory.create(
|
||||||
UserListPresenterArgs(
|
UserListPresenterArgs(
|
||||||
selectionMode = SelectionMode.Single,
|
selectionMode = SelectionMode.Single,
|
||||||
minimumSearchLength = 3,
|
|
||||||
searchDebouncePeriodMillis = UserListPresenterArgs.DEFAULT_DEBOUNCE,
|
|
||||||
),
|
),
|
||||||
userListDataSource,
|
userRepository,
|
||||||
userListDataStore,
|
userListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchQuery = it.userId.value,
|
searchQuery = it.userId.value,
|
||||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
searchResults = SearchBarResultState.Results(persistentListOf(it)),
|
||||||
selectedUsers = persistentListOf(it),
|
selectedUsers = persistentListOf(it),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
)
|
)
|
||||||
|
|
@ -43,7 +44,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchQuery = it.userId.value,
|
searchQuery = it.userId.value,
|
||||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
searchResults = SearchBarResultState.Results(persistentListOf(it)),
|
||||||
selectedUsers = persistentListOf(it),
|
selectedUsers = persistentListOf(it),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -32,7 +34,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -40,7 +41,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.userlist.api.components.UserListView
|
import io.element.android.features.createroom.impl.components.UserListView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
|
|
@ -55,6 +56,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.designsystem.R as DrawableR
|
import io.element.android.libraries.designsystem.R as DrawableR
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomRootView(
|
fun CreateRoomRootView(
|
||||||
state: CreateRoomRootState,
|
state: CreateRoomRootState,
|
||||||
|
|
@ -78,10 +80,11 @@ fun CreateRoomRootView(
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.consumeWindowInsets(paddingValues),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
UserListView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = state.userListState,
|
state = state.userListState,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.impl
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -28,24 +28,16 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
class DefaultUserListPresenter @AssistedInject constructor(
|
class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Assisted val args: UserListPresenterArgs,
|
@Assisted val args: UserListPresenterArgs,
|
||||||
@Assisted val userListDataSource: UserListDataSource,
|
@Assisted val userRepository: UserRepository,
|
||||||
@Assisted val userListDataStore: UserListDataStore,
|
@Assisted val userListDataStore: UserListDataStore,
|
||||||
) : UserListPresenter {
|
) : UserListPresenter {
|
||||||
|
|
||||||
|
|
@ -54,7 +46,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
interface DefaultUserListFactory : UserListPresenter.Factory {
|
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
args: UserListPresenterArgs,
|
args: UserListPresenterArgs,
|
||||||
userListDataSource: UserListDataSource,
|
userRepository: UserRepository,
|
||||||
userListDataStore: UserListDataStore,
|
userListDataStore: UserListDataStore,
|
||||||
): DefaultUserListPresenter
|
): DefaultUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
@ -64,24 +56,18 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchResults: UserSearchResultState by remember {
|
var searchResults: SearchBarResultState<ImmutableList<MatrixUser>> by remember {
|
||||||
mutableStateOf(UserSearchResultState.NotSearching)
|
mutableStateOf(SearchBarResultState.NotSearching())
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
searchResults = SearchBarResultState.NotSearching()
|
||||||
searchResults = if (MatrixPatterns.isUserId(searchQuery)) {
|
|
||||||
UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(searchQuery))))
|
|
||||||
} else {
|
|
||||||
UserSearchResultState.NotSearching
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce
|
userRepository.search(searchQuery).collect {
|
||||||
delay(args.searchDebouncePeriodMillis)
|
searchResults = when {
|
||||||
|
it.isEmpty() -> SearchBarResultState.NoResults()
|
||||||
// Perform the search asynchronously
|
else -> SearchBarResultState.Results(it.toImmutableList())
|
||||||
if (searchQuery.length >= args.minimumSearchLength) {
|
}
|
||||||
searchResults = performSearch(searchQuery)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,15 +87,4 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performSearch(query: String): UserSearchResultState {
|
|
||||||
val isMatrixId = MatrixPatterns.isUserId(query)
|
|
||||||
val results = userListDataSource.search(query).toMutableList()
|
|
||||||
if (isMatrixId && results.none { it.userId.value == query }) {
|
|
||||||
val getProfileResult: MatrixUser? = userListDataSource.getProfile(UserId(query))
|
|
||||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
|
||||||
results.add(0, profile)
|
|
||||||
}
|
|
||||||
return if (results.isEmpty()) UserSearchResultState.NoResults else UserSearchResultState.Results(results.toImmutableList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
|
|
@ -14,16 +14,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
|
||||||
interface UserListPresenter : Presenter<UserListState> {
|
interface UserListPresenter : Presenter<UserListState> {
|
||||||
|
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
args: UserListPresenterArgs,
|
args: UserListPresenterArgs,
|
||||||
userListDataSource: UserListDataSource,
|
userRepository: UserRepository,
|
||||||
userListDataStore: UserListDataStore,
|
userListDataStore: UserListDataStore,
|
||||||
): UserListPresenter
|
): UserListPresenter
|
||||||
}
|
}
|
||||||
|
|
@ -14,18 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
data class UserListPresenterArgs(
|
data class UserListPresenterArgs(
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
val minimumSearchLength: Int = 1,
|
)
|
||||||
val searchDebouncePeriodMillis: Long = NO_DEBOUNCE,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val NO_DEBOUNCE = 0L
|
|
||||||
const val DEFAULT_DEBOUNCE = 500L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class SelectionMode {
|
enum class SelectionMode {
|
||||||
Single,
|
Single,
|
||||||
|
|
@ -14,14 +14,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class UserListState(
|
data class UserListState(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: UserSearchResultState,
|
val searchResults: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
|
|
@ -29,14 +30,3 @@ data class UserListState(
|
||||||
) {
|
) {
|
||||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface UserSearchResultState {
|
|
||||||
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
|
||||||
object NotSearching : UserSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, but no results were found. */
|
|
||||||
object NoResults : UserSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, and some matching users were found. */
|
|
||||||
data class Results(val results: ImmutableList<MatrixUser>) : UserSearchResultState
|
|
||||||
}
|
|
||||||
|
|
@ -14,9 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -37,19 +38,19 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "something-with-no-results",
|
searchQuery = "something-with-no-results",
|
||||||
searchResults = UserSearchResultState.NoResults
|
searchResults = SearchBarResultState.NoResults()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +58,7 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
fun aUserListState() = UserListState(
|
fun aUserListState() = UserListState(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
searchResults = UserSearchResultState.NotSearching,
|
searchResults = SearchBarResultState.NotSearching(),
|
||||||
selectedUsers = persistentListOf(),
|
selectedUsers = persistentListOf(),
|
||||||
selectionMode = SelectionMode.Single,
|
selectionMode = SelectionMode.Single,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
@ -21,9 +21,9 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -34,7 +34,11 @@ class AddPeoplePresenterTests {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore(UserListDataStore()))
|
presenter = AddPeoplePresenter(
|
||||||
|
FakeUserListPresenterFactory(),
|
||||||
|
FakeUserRepository(),
|
||||||
|
CreateRoomDataStore(UserListDataStore())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -42,6 +46,7 @@ class AddPeoplePresenterTests {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
// TODO This doesn't actually test anything...
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState)
|
assertThat(initialState)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,10 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenter
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -32,6 +31,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
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.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
@ -39,7 +39,7 @@ import org.junit.Test
|
||||||
|
|
||||||
class CreateRoomRootPresenterTests {
|
class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
private lateinit var userListDataSource: FakeUserListDataSource
|
private lateinit var userRepository: FakeUserRepository
|
||||||
private lateinit var presenter: CreateRoomRootPresenter
|
private lateinit var presenter: CreateRoomRootPresenter
|
||||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||||
|
|
@ -48,8 +48,8 @@ class CreateRoomRootPresenterTests {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
fakeUserListPresenter = FakeUserListPresenter()
|
fakeUserListPresenter = FakeUserListPresenter()
|
||||||
fakeMatrixClient = FakeMatrixClient()
|
fakeMatrixClient = FakeMatrixClient()
|
||||||
userListDataSource = FakeUserListDataSource()
|
userRepository = FakeUserRepository()
|
||||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, UserListDataStore(), fakeMatrixClient)
|
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userRepository, UserListDataStore(), fakeMatrixClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -14,77 +14,78 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.impl
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultUserListPresenterTests {
|
class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
private val userListDataSource = FakeUserListDataSource()
|
private val userRepository = FakeUserRepository()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for single selection`() = runTest {
|
fun `present - initial state for single selection`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.searchQuery).isEmpty()
|
assertThat(initialState.searchQuery).isEmpty()
|
||||||
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.selectedUsers).isEmpty()
|
assertThat(initialState.selectedUsers).isEmpty()
|
||||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for multiple selection`() = runTest {
|
fun `present - initial state for multiple selection`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.searchQuery).isEmpty()
|
assertThat(initialState.searchQuery).isEmpty()
|
||||||
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.selectedUsers).isEmpty()
|
assertThat(initialState.selectedUsers).isEmpty()
|
||||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - update search query`() = runTest {
|
fun `present - update search query`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
||||||
|
|
@ -93,12 +94,14 @@ class DefaultUserListPresenterTests {
|
||||||
val matrixIdQuery = "@name:matrix.org"
|
val matrixIdQuery = "@name:matrix.org"
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(matrixIdQuery)))))
|
assertThat(userRepository.providedQuery).isEqualTo(matrixIdQuery)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
val notMatrixIdQuery = "name"
|
val notMatrixIdQuery = "name"
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NoResults)
|
assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
assertThat(awaitItem().isSearchActive).isFalse()
|
||||||
|
|
@ -106,39 +109,83 @@ class DefaultUserListPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - searches when minimum length exceeded`() = runTest {
|
fun `present - presents search results`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single, minimumSearchLength = 3),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(
|
||||||
UserListDataStore(),
|
selectionMode = SelectionMode.Single,
|
||||||
)
|
),
|
||||||
|
userRepository,
|
||||||
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
// When the search term is too short, nothing happens
|
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("al"))
|
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
|
||||||
|
|
||||||
// When it reaches the minimum length, a search is performed asynchronously
|
|
||||||
userListDataSource.givenSearchResult(listOf(aMatrixUser()))
|
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(aMatrixUser())))
|
assertThat(userRepository.providedQuery).isEqualTo("alice")
|
||||||
|
skipItems(2)
|
||||||
|
|
||||||
|
// When the user repository emits a result, it's copied to the state
|
||||||
|
userRepository.emitResult(listOf(aMatrixUser()))
|
||||||
|
assertThat(awaitItem().searchResults).isEqualTo(
|
||||||
|
SearchBarResultState.Results(
|
||||||
|
persistentListOf(aMatrixUser())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// When the user repository emits another result, it replaces the previous value
|
||||||
|
userRepository.emitResult(aMatrixUserList())
|
||||||
|
assertThat(awaitItem().searchResults).isEqualTo(
|
||||||
|
SearchBarResultState.Results(
|
||||||
|
aMatrixUserList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - presents search results when not found`() = runTest {
|
||||||
|
val presenter =
|
||||||
|
DefaultUserListPresenter(
|
||||||
|
UserListPresenterArgs(
|
||||||
|
selectionMode = SelectionMode.Single,
|
||||||
|
),
|
||||||
|
userRepository,
|
||||||
|
UserListDataStore(),
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
|
||||||
|
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||||
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
|
assertThat(userRepository.providedQuery).isEqualTo("alice")
|
||||||
|
skipItems(2)
|
||||||
|
|
||||||
|
// When the results list is empty, the state is set to NoResults
|
||||||
|
userRepository.emitResult(emptyList())
|
||||||
|
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - select a user`() = runTest {
|
fun `present - select a user`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
val userA = aMatrixUser("@userA:domain", "A")
|
val userA = aMatrixUser("@userA:domain", "A")
|
||||||
|
|
@ -14,12 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
|
||||||
|
|
||||||
class FakeUserListPresenter : UserListPresenter {
|
class FakeUserListPresenter : UserListPresenter {
|
||||||
|
|
||||||
|
|
@ -14,12 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
|
|
||||||
class FakeUserListPresenterFactory(
|
class FakeUserListPresenterFactory(
|
||||||
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
||||||
|
|
@ -27,7 +24,7 @@ class FakeUserListPresenterFactory(
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
args: UserListPresenterArgs,
|
args: UserListPresenterArgs,
|
||||||
userListDataSource: UserListDataSource,
|
userRepository: UserRepository,
|
||||||
userListDataStore: UserListDataStore,
|
userListDataStore: UserListDataStore,
|
||||||
): UserListPresenter = fakeUserListPresenter
|
): UserListPresenter = fakeUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invitelist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
|
@ -103,7 +105,7 @@ fun InviteListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun InviteListContent(
|
fun InviteListContent(
|
||||||
state: InviteListState,
|
state: InviteListState,
|
||||||
|
|
@ -124,7 +126,9 @@ fun InviteListContent(
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(padding)
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
if (state.inviteList.isEmpty()) {
|
if (state.inviteList.isEmpty()) {
|
||||||
Spacer(Modifier.size(80.dp))
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -81,7 +83,7 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeServerView(
|
fun ChangeServerView(
|
||||||
state: ChangeServerState,
|
state: ChangeServerState,
|
||||||
|
|
@ -121,6 +123,7 @@ fun ChangeServerView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -86,7 +88,7 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginRootView(
|
fun LoginRootView(
|
||||||
state: LoginRootState,
|
state: LoginRootState,
|
||||||
|
|
@ -113,6 +115,7 @@ fun LoginRootView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ package io.element.android.features.messages.impl
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
|
@ -79,7 +81,7 @@ import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesView(
|
fun MessagesView(
|
||||||
state: MessagesState,
|
state: MessagesState,
|
||||||
|
|
@ -183,7 +185,9 @@ fun MessagesView(
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
MessagesViewContent(
|
MessagesViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
onMessageClicked = ::onMessageClicked,
|
onMessageClicked = ::onMessageClicked,
|
||||||
onMessageLongClicked = ::onMessageLongClicked
|
onMessageLongClicked = ::onMessageLongClicked
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
api(projects.features.roomdetails.api)
|
api(projects.features.roomdetails.api)
|
||||||
|
api(projects.libraries.usersearch.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -50,7 +51,6 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ package io.element.android.features.roomdetails.impl
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -43,8 +45,8 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
||||||
import io.element.android.libraries.architecture.isLoading
|
import io.element.android.libraries.architecture.isLoading
|
||||||
|
|
@ -68,7 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomDetailsView(
|
fun RoomDetailsView(
|
||||||
state: RoomDetailsState,
|
state: RoomDetailsState,
|
||||||
|
|
@ -91,6 +93,7 @@ fun RoomDetailsView(
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
when (state.roomType) {
|
when (state.roomType) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.blockuser
|
package io.element.android.features.roomdetails.impl.blockuser
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Block
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -41,7 +42,7 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchResults by remember {
|
var searchResults by remember {
|
||||||
mutableStateOf<RoomMemberSearchResultState>(RoomMemberSearchResultState.NotSearching)
|
mutableStateOf<SearchBarResultState<RoomMembers>>(SearchBarResultState.NotSearching())
|
||||||
}
|
}
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -60,11 +61,11 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
searchResults = if (searchQuery.isEmpty()) {
|
searchResults = if (searchQuery.isEmpty()) {
|
||||||
RoomMemberSearchResultState.NotSearching
|
SearchBarResultState.NotSearching()
|
||||||
} else {
|
} else {
|
||||||
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
||||||
if (results.isEmpty()) RoomMemberSearchResultState.NoResults
|
if (results.isEmpty()) SearchBarResultState.NoResults()
|
||||||
else RoomMemberSearchResultState.Results(
|
else SearchBarResultState.Results(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomMemberListState(
|
data class RoomMemberListState(
|
||||||
val roomMembers: Async<RoomMembers>,
|
val roomMembers: Async<RoomMembers>,
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: RoomMemberSearchResultState,
|
val searchResults: SearchBarResultState<RoomMembers>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
@ -32,14 +33,3 @@ data class RoomMembers(
|
||||||
val invited: ImmutableList<RoomMember>,
|
val invited: ImmutableList<RoomMember>,
|
||||||
val joined: ImmutableList<RoomMember>
|
val joined: ImmutableList<RoomMember>
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface RoomMemberSearchResultState {
|
|
||||||
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
|
||||||
object NotSearching : RoomMemberSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, but no results were found. */
|
|
||||||
object NoResults : RoomMemberSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, and some matching users were found. */
|
|
||||||
data class Results(val results: RoomMembers) : RoomMemberSearchResultState
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||||
override val values: Sequence<RoomMemberListState>
|
override val values: Sequence<RoomMemberListState>
|
||||||
|
|
@ -42,7 +42,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
aRoomMemberListState().copy(
|
aRoomMemberListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
searchResults = RoomMemberSearchResultState.Results(
|
searchResults = SearchBarResultState.Results(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = persistentListOf(aVictor()),
|
invited = persistentListOf(aVictor()),
|
||||||
joined = persistentListOf(anAlice()),
|
joined = persistentListOf(anAlice()),
|
||||||
|
|
@ -52,14 +52,14 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
aRoomMemberListState().copy(
|
aRoomMemberListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "something-with-no-results",
|
searchQuery = "something-with-no-results",
|
||||||
searchResults = RoomMemberSearchResultState.NoResults
|
searchResults = SearchBarResultState.NoResults()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomMemberListState(
|
internal fun aRoomMemberListState(
|
||||||
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||||
searchResults: RoomMemberSearchResultState = RoomMemberSearchResultState.NotSearching,
|
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.NotSearching(),
|
||||||
) = RoomMemberListState(
|
) = RoomMemberListState(
|
||||||
roomMembers = roomMembers,
|
roomMembers = roomMembers,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,20 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -59,10 +52,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.CenterAlignedTopAppBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
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.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
@ -71,6 +63,7 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListView(
|
fun RoomMemberListView(
|
||||||
state: RoomMemberListState,
|
state: RoomMemberListState,
|
||||||
|
|
@ -93,21 +86,20 @@ fun RoomMemberListView(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
RoomMemberSearchBar(
|
||||||
RoomMemberSearchBar(
|
query = state.searchQuery,
|
||||||
query = state.searchQuery,
|
state = state.searchResults,
|
||||||
state = state.searchResults,
|
active = state.isSearchActive,
|
||||||
active = state.isSearchActive,
|
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
||||||
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
||||||
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
||||||
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
onUserSelected = ::onUserSelected,
|
||||||
onUserSelected = ::onUserSelected,
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
if (state.roomMembers is Async.Success) {
|
if (state.roomMembers is Async.Success) {
|
||||||
|
|
@ -216,11 +208,10 @@ private fun RoomMemberListTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberSearchBar(
|
private fun RoomMemberSearchBar(
|
||||||
query: String,
|
query: String,
|
||||||
state: RoomMemberSearchResultState,
|
state: SearchBarResultState<RoomMembers>,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
placeHolderTitle: String,
|
placeHolderTitle: String,
|
||||||
onActiveChanged: (Boolean) -> Unit,
|
onActiveChanged: (Boolean) -> Unit,
|
||||||
|
|
@ -228,72 +219,21 @@ private fun RoomMemberSearchBar(
|
||||||
onUserSelected: (RoomMember) -> Unit,
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
if (!active) {
|
|
||||||
onTextChanged("")
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = query,
|
query = query,
|
||||||
onQueryChange = onTextChanged,
|
onQueryChange = onTextChanged,
|
||||||
onSearch = { focusManager.clearFocus() },
|
|
||||||
active = active,
|
active = active,
|
||||||
onActiveChange = onActiveChanged,
|
onActiveChange = onActiveChanged,
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
placeHolderTitle = placeHolderTitle,
|
||||||
placeholder = {
|
resultState = state,
|
||||||
Text(
|
resultHandler = { results ->
|
||||||
text = placeHolderTitle,
|
RoomMemberList(
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
roomMembers = results,
|
||||||
|
showMembersCount = false,
|
||||||
|
onUserSelected = { onUserSelected(it) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
leadingIcon = if (active) {
|
|
||||||
{ BackButton(onClick = { onActiveChanged(false) }) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
trailingIcon = when {
|
|
||||||
active && query.isNotEmpty() -> {
|
|
||||||
{
|
|
||||||
IconButton(onClick = { onTextChanged("") }) {
|
|
||||||
Icon(Icons.Default.Close, stringResource(StringR.string.action_clear))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!active -> {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = stringResource(StringR.string.action_search),
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
|
||||||
content = {
|
|
||||||
if (state is RoomMemberSearchResultState.Results) {
|
|
||||||
RoomMemberList(
|
|
||||||
roomMembers = state.results,
|
|
||||||
showMembersCount = false,
|
|
||||||
onUserSelected = onUserSelected
|
|
||||||
)
|
|
||||||
} else if (state is RoomMemberSearchResultState.NoResults) {
|
|
||||||
Spacer(Modifier.size(80.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(StringR.string.common_no_results),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ package io.element.android.features.roomdetails.impl.members.details
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -39,8 +41,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
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.AvatarData
|
||||||
|
|
@ -57,7 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberDetailsView(
|
fun RoomMemberDetailsView(
|
||||||
state: RoomMemberDetailsState,
|
state: RoomMemberDetailsState,
|
||||||
|
|
@ -74,6 +76,7 @@ fun RoomMemberDetailsView(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
RoomMemberHeaderSection(
|
RoomMemberHeaderSection(
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,12 @@ import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberSearchResultState
|
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||||
import io.element.android.features.roomdetails.impl.members.aWalter
|
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -49,7 +49,7 @@ class RoomMemberListPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
||||||
Truth.assertThat(initialState.searchQuery).isEmpty()
|
Truth.assertThat(initialState.searchQuery).isEmpty()
|
||||||
Truth.assertThat(initialState.searchResults).isEqualTo(RoomMemberSearchResultState.NotSearching)
|
Truth.assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
Truth.assertThat(initialState.isSearchActive).isFalse()
|
Truth.assertThat(initialState.isSearchActive).isFalse()
|
||||||
|
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
|
|
@ -89,7 +89,7 @@ class RoomMemberListPresenterTests {
|
||||||
val searchQueryUpdatedState = awaitItem()
|
val searchQueryUpdatedState = awaitItem()
|
||||||
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
||||||
val searchSearchResultDelivered = awaitItem()
|
val searchSearchResultDelivered = awaitItem()
|
||||||
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.NoResults::class.java)
|
Truth.assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,8 +107,8 @@ class RoomMemberListPresenterTests {
|
||||||
val searchQueryUpdatedState = awaitItem()
|
val searchQueryUpdatedState = awaitItem()
|
||||||
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
||||||
val searchSearchResultDelivered = awaitItem()
|
val searchSearchResultDelivered = awaitItem()
|
||||||
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.Results::class.java)
|
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
Truth.assertThat((searchSearchResultDelivered.searchResults as RoomMemberSearchResultState.Results).results.joined.first().displayName)
|
Truth.assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.joined.first().displayName)
|
||||||
.isEqualTo("Alice")
|
.isEqualTo("Alice")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -116,7 +118,7 @@ fun RoomListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListContent(
|
fun RoomListContent(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
|
|
@ -190,6 +192,7 @@ fun RoomListContent(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -89,7 +91,7 @@ internal fun RoomListSearchResultView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomListSearchResultContent(
|
internal fun RoomListSearchResultContent(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
|
|
@ -183,6 +185,7 @@ internal fun RoomListSearchResultContent(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.features.userlist.api.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
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.MaterialTheme
|
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
|
||||||
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.SearchBar
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import io.element.android.libraries.ui.strings.R
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun SearchUserBar(
|
|
||||||
query: String,
|
|
||||||
state: UserSearchResultState,
|
|
||||||
selectedUsers: ImmutableList<MatrixUser>,
|
|
||||||
active: Boolean,
|
|
||||||
isMultiSelectionEnabled: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
|
||||||
onActiveChanged: (Boolean) -> Unit = {},
|
|
||||||
onTextChanged: (String) -> Unit = {},
|
|
||||||
onUserSelected: (MatrixUser) -> Unit = {},
|
|
||||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
if (!active) {
|
|
||||||
onTextChanged("")
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchBar(
|
|
||||||
query = query,
|
|
||||||
onQueryChange = onTextChanged,
|
|
||||||
onSearch = { focusManager.clearFocus() },
|
|
||||||
active = active,
|
|
||||||
onActiveChange = onActiveChanged,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = placeHolderTitle,
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = if (active) {
|
|
||||||
{ BackButton(onClick = { onActiveChanged(false) }) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
trailingIcon = when {
|
|
||||||
active && query.isNotEmpty() -> {
|
|
||||||
{
|
|
||||||
IconButton(onClick = { onTextChanged("") }) {
|
|
||||||
Icon(Icons.Default.Close, stringResource(R.string.action_clear))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!active -> {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = stringResource(R.string.action_search),
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
|
||||||
content = {
|
|
||||||
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
|
||||||
SelectedUsersList(
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
selectedUsers = selectedUsers,
|
|
||||||
autoScroll = true,
|
|
||||||
onUserRemoved = onUserDeselected,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (state is UserSearchResultState.Results) {
|
|
||||||
LazyColumn {
|
|
||||||
if (isMultiSelectionEnabled) {
|
|
||||||
items(state.results) { matrixUser ->
|
|
||||||
SearchMultipleUsersResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
if (checked) {
|
|
||||||
onUserSelected(matrixUser)
|
|
||||||
} else {
|
|
||||||
onUserDeselected(matrixUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items(state.results) { matrixUser ->
|
|
||||||
SearchSingleUserResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
onClick = { onUserSelected(matrixUser) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (state is UserSearchResultState.NoResults) {
|
|
||||||
Spacer(Modifier.size(80.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.common_no_results),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -18,8 +18,10 @@ package io.element.android.libraries.designsystem.components.preferences
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -45,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreferenceView(
|
fun PreferenceView(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -69,6 +72,7 @@ fun PreferenceView(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(it)
|
.padding(it)
|
||||||
|
.consumeWindowInsets(it)
|
||||||
.verticalScroll(
|
.verticalScroll(
|
||||||
state = scrollState,
|
state = scrollState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,74 +16,216 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SearchBarColors
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun <T> SearchBar(
|
||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
onActiveChange: (Boolean) -> Unit,
|
onActiveChange: (Boolean) -> Unit,
|
||||||
|
placeHolderTitle: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
placeholder: @Composable (() -> Unit)? = null,
|
resultState: SearchBarResultState<T> = SearchBarResultState.NotSearching(),
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
|
||||||
tonalElevation: Dp = SearchBarDefaults.Elevation,
|
tonalElevation: Dp = SearchBarDefaults.Elevation,
|
||||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
resultHandler: @Composable ColumnScope.(T) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
onQueryChange("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
androidx.compose.material3.SearchBar(
|
androidx.compose.material3.SearchBar(
|
||||||
query = query,
|
query = query,
|
||||||
onQueryChange = onQueryChange,
|
onQueryChange = onQueryChange,
|
||||||
onSearch = onSearch,
|
onSearch = { focusManager.clearFocus() },
|
||||||
active = active,
|
active = active,
|
||||||
onActiveChange = onActiveChange,
|
onActiveChange = onActiveChange,
|
||||||
modifier = modifier,
|
modifier = modifier.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
placeholder = placeholder,
|
placeholder = {
|
||||||
leadingIcon = leadingIcon,
|
Text(
|
||||||
trailingIcon = trailingIcon,
|
text = placeHolderTitle,
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = if (active) {
|
||||||
|
{ BackButton(onClick = { onActiveChange(false) }) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
trailingIcon = when {
|
||||||
|
active && query.isNotEmpty() -> {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onQueryChange("") }) {
|
||||||
|
Icon(Icons.Default.Close, stringResource(R.string.action_clear))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!active -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = stringResource(R.string.action_search),
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
shape = shape,
|
shape = shape,
|
||||||
colors = colors,
|
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
windowInsets = windowInsets,
|
windowInsets = windowInsets,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
content = content,
|
content = {
|
||||||
|
contentPrefix()
|
||||||
|
when (resultState) {
|
||||||
|
is SearchBarResultState.Results<T> -> {
|
||||||
|
resultHandler(resultState.results)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchBarResultState.NoResults<T> -> {
|
||||||
|
// No results found, show a message
|
||||||
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.common_no_results),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Not searching - nothing to show.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentSuffix()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SearchBarResultState<in T> {
|
||||||
|
/** No search results are available yet (e.g. because the user hasn't entered a search term). */
|
||||||
|
class NotSearching<T> : SearchBarResultState<T>
|
||||||
|
|
||||||
|
/** The search has completed, but no results were found. */
|
||||||
|
class NoResults<T> : SearchBarResultState<T>
|
||||||
|
|
||||||
|
/** The search has completed, and some matching users were found. */
|
||||||
|
data class Results<T>(val results: T) : SearchBarResultState<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Search)
|
||||||
|
@Composable
|
||||||
|
internal fun SearchBarPreviewInactive() = ElementThemedPreview { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Search)
|
||||||
|
@Composable
|
||||||
|
internal fun SearchBarPreviewActiveEmptyQuery() = ElementThemedPreview {
|
||||||
|
ContentToPreview(
|
||||||
|
query = "",
|
||||||
|
active = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Search)
|
@Preview(group = PreviewGroup.Search)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SearchBarPreview() = ElementThemedPreview { ContentToPreview() }
|
internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview {
|
||||||
|
ContentToPreview(
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
query = "search term",
|
||||||
@Composable
|
active = true,
|
||||||
private fun ContentToPreview() {
|
)
|
||||||
SearchBar(
|
}
|
||||||
query = "Some text",
|
|
||||||
onQueryChange = {},
|
@Preview(group = PreviewGroup.Search)
|
||||||
onSearch = {},
|
@Composable
|
||||||
active = false,
|
internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview {
|
||||||
onActiveChange = {},
|
ContentToPreview(
|
||||||
content = {},
|
query = "search term",
|
||||||
|
active = true,
|
||||||
|
resultState = SearchBarResultState.NoResults(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Search)
|
||||||
|
@Composable
|
||||||
|
internal fun SearchBarPreviewActiveWithContent() = ElementThemedPreview {
|
||||||
|
ContentToPreview(
|
||||||
|
query = "search term",
|
||||||
|
active = true,
|
||||||
|
resultState = SearchBarResultState.Results("result!"),
|
||||||
|
contentPrefix = {
|
||||||
|
Text(text = "Content that goes before the search results", modifier = Modifier.background(color = Color.Red).fillMaxWidth())
|
||||||
|
},
|
||||||
|
contentSuffix = {
|
||||||
|
Text(text = "Content that goes after the search results", modifier = Modifier.background(color = Color.Blue).fillMaxWidth())
|
||||||
|
},
|
||||||
|
resultHandler = {
|
||||||
|
Text(text = "Results go here", modifier = Modifier.background(color = Color.Green).fillMaxWidth())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(
|
||||||
|
query: String = "",
|
||||||
|
active: Boolean = false,
|
||||||
|
resultState: SearchBarResultState<String> = SearchBarResultState.NotSearching(),
|
||||||
|
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
resultHandler: @Composable ColumnScope.(String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
active = active,
|
||||||
|
resultState = resultState,
|
||||||
|
onQueryChange = {},
|
||||||
|
onActiveChange = {},
|
||||||
|
placeHolderTitle = "Search for things",
|
||||||
|
contentPrefix = contentPrefix,
|
||||||
|
contentSuffix = contentSuffix,
|
||||||
|
resultHandler = resultHandler,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,14 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
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.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CheckableMatrixUserRow(
|
fun CheckableMatrixUserRow(
|
||||||
|
|
@ -40,18 +43,39 @@ fun CheckableMatrixUserRow(
|
||||||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||||
onCheckedChange: (Boolean) -> Unit = {},
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) = CheckableUserRow(
|
||||||
|
checked = checked,
|
||||||
|
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||||
|
name = matrixUser.getBestName(),
|
||||||
|
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||||
|
modifier = modifier,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
enabled = enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CheckableUserRow(
|
||||||
|
checked: Boolean,
|
||||||
|
avatarData: AvatarData,
|
||||||
|
name: String,
|
||||||
|
subtext: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(role = Role.Checkbox) { onCheckedChange(!checked) },
|
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||||
|
onCheckedChange(!checked)
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
MatrixUserRow(
|
UserRow(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
matrixUser = matrixUser,
|
avatarData = avatarData,
|
||||||
avatarSize = avatarSize,
|
name = name,
|
||||||
|
subtext = subtext,
|
||||||
)
|
)
|
||||||
|
|
||||||
Checkbox(
|
Checkbox(
|
||||||
|
|
@ -77,5 +101,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
Column {
|
Column {
|
||||||
CheckableMatrixUserRow(checked = true, matrixUser)
|
CheckableMatrixUserRow(checked = true, matrixUser)
|
||||||
CheckableMatrixUserRow(checked = false, matrixUser)
|
CheckableMatrixUserRow(checked = false, matrixUser)
|
||||||
|
CheckableMatrixUserRow(checked = true, matrixUser, enabled = false)
|
||||||
|
CheckableMatrixUserRow(checked = false, matrixUser, enabled = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
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.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -46,6 +47,19 @@ fun MatrixUserRow(
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||||
|
) = UserRow(
|
||||||
|
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||||
|
name = matrixUser.getBestName(),
|
||||||
|
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserRow(
|
||||||
|
avatarData: AvatarData,
|
||||||
|
name: String,
|
||||||
|
subtext: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -54,9 +68,7 @@ fun MatrixUserRow(
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(avatarData)
|
||||||
matrixUser.getAvatarData(size = avatarSize),
|
|
||||||
)
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp),
|
.padding(start = 12.dp),
|
||||||
|
|
@ -65,15 +77,15 @@ fun MatrixUserRow(
|
||||||
Text(
|
Text(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
text = matrixUser.getBestName(),
|
text = name,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
// Id
|
// Id
|
||||||
if (matrixUser.displayName.isNullOrEmpty().not()) {
|
subtext?.let {
|
||||||
Text(
|
Text(
|
||||||
text = matrixUser.userId.value,
|
text = subtext,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -41,10 +41,9 @@ 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.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedUser(
|
fun SelectedUser(
|
||||||
|
|
@ -74,7 +73,7 @@ fun SelectedUser(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = stringResource(id = R.string.action_remove),
|
contentDescription = stringResource(id = StringR.string.action_remove),
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -30,11 +30,11 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedUsersList(
|
fun SelectedUsersList(
|
||||||
|
|
@ -82,6 +82,6 @@ internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPrev
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
SelectedUsersList(
|
SelectedUsersList(
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -85,14 +85,14 @@
|
||||||
<string name="common_report_a_bug">"Report a bug"</string>
|
<string name="common_report_a_bug">"Report a bug"</string>
|
||||||
<string name="common_report_submitted">"Report submitted"</string>
|
<string name="common_report_submitted">"Report submitted"</string>
|
||||||
<string name="common_search_for_someone">"Search for someone"</string>
|
<string name="common_search_for_someone">"Search for someone"</string>
|
||||||
<string name="common_search_results">"Search results"</string>
|
<string name="common_search_results">"Search results"</string>
|
||||||
<string name="common_security">"Security"</string>
|
<string name="common_security">"Security"</string>
|
||||||
<string name="common_select_your_server">"Select your server"</string>
|
<string name="common_select_your_server">"Select your server"</string>
|
||||||
<string name="common_sending">"Sending…"</string>
|
<string name="common_sending">"Sending…"</string>
|
||||||
<string name="common_server_not_supported">"Server not supported"</string>
|
<string name="common_server_not_supported">"Server not supported"</string>
|
||||||
<string name="common_server_url">"Server URL"</string>
|
<string name="common_server_url">"Server URL"</string>
|
||||||
<string name="common_settings">"Settings"</string>
|
<string name="common_settings">"Settings"</string>
|
||||||
<string name="common_starting_chat">"Starting chat…"</string>
|
<string name="common_starting_chat">"Starting chat…"</string>
|
||||||
<string name="common_sticker">"Sticker"</string>
|
<string name="common_sticker">"Sticker"</string>
|
||||||
<string name="common_success">"Success"</string>
|
<string name="common_success">"Success"</string>
|
||||||
<string name="common_suggestions">"Suggestions"</string>
|
<string name="common_suggestions">"Suggestions"</string>
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,16 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-library")
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.userlist.api"
|
namespace = "io.element.android.libraries.usersearch.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
|
||||||
ksp(libs.showkase.processor)
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.libraries.usersearch.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -14,22 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.di
|
package io.element.android.libraries.usersearch.api
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import dagger.Binds
|
import kotlinx.coroutines.flow.Flow
|
||||||
import dagger.Module
|
|
||||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
@Module
|
interface UserRepository {
|
||||||
@ContributesTo(SessionScope::class)
|
|
||||||
interface CreateRoomModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Named("AllUsers")
|
|
||||||
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): UserListDataSource
|
|
||||||
|
|
||||||
|
suspend fun search(query: String): Flow<List<MatrixUser>>
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -15,13 +15,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-library")
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.userlist.impl"
|
namespace = "io.element.android.libraries.usersearch.impl"
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -29,28 +28,18 @@ anvil {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.anvilannotations)
|
|
||||||
anvil(projects.anvilcodegen)
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.elementresources)
|
api(projects.libraries.usersearch.api)
|
||||||
implementation(projects.libraries.testtags)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
|
||||||
api(projects.features.userlist.api)
|
|
||||||
ksp(libs.showkase.processor)
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.coroutines.core)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,15 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.libraries.usersearch.impl
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AllMatrixUsersDataSource @Inject constructor(
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class MatrixUserListDataSource @Inject constructor(
|
||||||
private val client: MatrixClient
|
private val client: MatrixClient
|
||||||
) : UserListDataSource {
|
) : UserListDataSource {
|
||||||
override suspend fun search(query: String): List<MatrixUser> {
|
override suspend fun search(query: String): List<MatrixUser> {
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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.usersearch.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class MatrixUserRepository @Inject constructor(
|
||||||
|
private val dataSource: UserListDataSource
|
||||||
|
) : UserRepository {
|
||||||
|
|
||||||
|
override suspend fun search(query: String): Flow<List<MatrixUser>> = flow {
|
||||||
|
// Manually add a fake result with the matrixId, if any
|
||||||
|
val isUserId = MatrixPatterns.isUserId(query)
|
||||||
|
if (isUserId) {
|
||||||
|
emit(listOf(MatrixUser(UserId(query))))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length >= MINIMUM_SEARCH_LENGTH) {
|
||||||
|
// Debounce
|
||||||
|
delay(DEBOUNCE_TIME_MILLIS)
|
||||||
|
|
||||||
|
val results = dataSource.search(query).toMutableList()
|
||||||
|
|
||||||
|
// If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly
|
||||||
|
if (isUserId && results.none { it.userId.value == query }) {
|
||||||
|
val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query))
|
||||||
|
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||||
|
results.add(0, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEBOUNCE_TIME_MILLIS = 500L
|
||||||
|
private const val MINIMUM_SEARCH_LENGTH = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.libraries.usersearch.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
internal class AllMatrixUsersDataSourceTest {
|
internal class MatrixUserListDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `search - returns users on success`() = runTest {
|
fun `search - returns users on success`() = runTest {
|
||||||
|
|
@ -37,15 +37,21 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
searchTerm = "test",
|
searchTerm = "test",
|
||||||
result = Result.success(
|
result = Result.success(
|
||||||
MatrixSearchUserResults(
|
MatrixSearchUserResults(
|
||||||
results = listOf(aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2)),
|
results = listOf(
|
||||||
|
aMatrixUserProfile(),
|
||||||
|
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||||
|
),
|
||||||
limited = false
|
limited = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val results = dataSource.search("test")
|
val results = dataSource.search("test")
|
||||||
Truth.assertThat(results).containsExactly(aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2))
|
Truth.assertThat(results).containsExactly(
|
||||||
|
aMatrixUserProfile(),
|
||||||
|
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -55,7 +61,7 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
searchTerm = "test",
|
searchTerm = "test",
|
||||||
result = Result.failure(Throwable("Ruhroh"))
|
result = Result.failure(Throwable("Ruhroh"))
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val results = dataSource.search("test")
|
val results = dataSource.search("test")
|
||||||
Truth.assertThat(results).isEmpty()
|
Truth.assertThat(results).isEmpty()
|
||||||
|
|
@ -68,7 +74,7 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
result = Result.success(aMatrixUserProfile())
|
result = Result.success(aMatrixUserProfile())
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val result = dataSource.getProfile(A_USER_ID)
|
val result = dataSource.getProfile(A_USER_ID)
|
||||||
Truth.assertThat(result).isEqualTo(aMatrixUserProfile())
|
Truth.assertThat(result).isEqualTo(aMatrixUserProfile())
|
||||||
|
|
@ -81,7 +87,7 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
result = Result.failure(Throwable("Ruhroh"))
|
result = Result.failure(Throwable("Ruhroh"))
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val result = dataSource.getProfile(A_USER_ID)
|
val result = dataSource.getProfile(A_USER_ID)
|
||||||
Truth.assertThat(result).isNull()
|
Truth.assertThat(result).isNull()
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* 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.usersearch.impl
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
internal class MatrixUserRepositoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - emits nothing if the search query is too short`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search("x")
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - returns empty list if no results are found`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search("some query")
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
assertThat(awaitItem()).isEmpty()
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - returns users if results are found`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(aMatrixUserList())
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search("some query")
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(aMatrixUserList())
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - immediately returns placeholder if search is mxid`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)))
|
||||||
|
skipItems(1)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - does not change results if they contain searched mxid`() = runTest {
|
||||||
|
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID) + MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(searchResults)
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(searchResults)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - gets profile results if searched mxid not in results`() = runTest {
|
||||||
|
val userProfile = MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||||
|
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||||
|
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(searchResults)
|
||||||
|
dataSource.givenUserProfile(userProfile)
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - just shows id if profile can't be loaded`() = runTest {
|
||||||
|
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||||
|
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(searchResults)
|
||||||
|
dataSource.givenUserProfile(null)
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -13,18 +13,18 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.userlist.test"
|
namespace = "io.element.android.libraries.usersearch"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
api(projects.features.userlist.api)
|
api(projects.libraries.usersearch.api)
|
||||||
api(libs.coroutines.core)
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.libraries.usersearch.test
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||||
|
|
||||||
class FakeUserListDataSource : UserListDataSource {
|
class FakeUserListDataSource : UserListDataSource {
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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.usersearch.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
|
class FakeUserRepository : UserRepository {
|
||||||
|
|
||||||
|
var providedQuery: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val flow = MutableSharedFlow<List<MatrixUser>>()
|
||||||
|
|
||||||
|
override suspend fun search(query: String): Flow<List<MatrixUser>> {
|
||||||
|
providedQuery = query
|
||||||
|
return flow
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun emitResult(result: List<MatrixUser>) {
|
||||||
|
flow.emit(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -90,6 +90,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:statemachine"))
|
implementation(project(":libraries:statemachine"))
|
||||||
implementation(project(":libraries:mediapickers:impl"))
|
implementation(project(":libraries:mediapickers:impl"))
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
|
implementation(project(":libraries:usersearch:impl"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.allServicesImpl() {
|
fun DependencyHandlerScope.allServicesImpl() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cbfbfa08085539bbfa4f20bc5d149bf0db0036169a32747c96f956d0d0f4b42d
|
oid sha256:b7787897d8dda469f17da7151260f26b6dcc5500f93ce44fb317af42ae8d457c
|
||||||
size 93741
|
size 93739
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9e0ba60e784a0dc582826a3f5679a6015531dc9aca22617ec1e0dede38b97f30
|
oid sha256:6691512398d2c7e319d15397120054f1c2aae452ffed0be47df72ecb906ac4fa
|
||||||
size 93678
|
size 93675
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:766b2d44b51a36d1b15d8a39434ce5237f8427d1150f9b4c1255043fc1e4bb79
|
oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81
|
||||||
size 492165
|
size 492167
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:766b2d44b51a36d1b15d8a39434ce5237f8427d1150f9b4c1255043fc1e4bb79
|
oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81
|
||||||
size 492165
|
size 492167
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08
|
oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce
|
||||||
size 3330
|
size 3332
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f
|
oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f
|
||||||
size 4000
|
size 4001
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08
|
oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce
|
||||||
size 3330
|
size 3332
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f
|
oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f
|
||||||
size 4000
|
size 4001
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1
|
oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8
|
||||||
size 3142
|
size 3138
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67
|
oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71
|
||||||
size 3834
|
size 3829
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1
|
oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8
|
||||||
size 3142
|
size 3138
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67
|
oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71
|
||||||
size 3834
|
size 3829
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6cab21ee595c96700e4577554f9c98fdc56a59bf5093aa9d81a54d10381ba122
|
oid sha256:25d0334932490997d432115c550c42e74e7226399ef82031430bf86ba33972bd
|
||||||
size 6065
|
size 6066
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b9235621126fe90a51d557df616878d53120dada814830fba006b54f1556e69e
|
oid sha256:a2d15387248308e5234c84b9b2b282e736f05f0495c80807bacfb0acff31eb2b
|
||||||
size 5895
|
size 5890
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:562e83b7771ff2209c7882180af55a8f3123c12c020b294858b1b46bff8a8a95
|
oid sha256:5315b975f8b9def0b0d7575160cb34300511f1b83845ee2d1762188481cf6fe9
|
||||||
size 30928
|
size 30927
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue