Merge remote-tracking branch 'origin/develop' into feature/fre/invite_people_action
This commit is contained in:
commit
44a3f48306
138 changed files with 1022 additions and 697 deletions
|
|
@ -46,10 +46,10 @@ dependencies {
|
|||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.features.userlist.api)
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.libraries.usersearch.impl)
|
||||
api(projects.features.createroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
@ -60,10 +60,9 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.impl)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.createroom.impl
|
|||
import android.net.Uri
|
||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
|
|||
|
|
@ -18,18 +18,17 @@ package io.element.android.features.createroom.impl.addpeople
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
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.createroom.impl.userlist.SelectionMode
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class AddPeoplePresenter @Inject constructor(
|
||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
||||
private val userRepository: UserRepository,
|
||||
private val dataStore: CreateRoomDataStore,
|
||||
) : Presenter<UserListState> {
|
||||
|
||||
|
|
@ -37,10 +36,8 @@ class AddPeoplePresenter @Inject constructor(
|
|||
userListPresenterFactory.create(
|
||||
UserListPresenterArgs(
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
minimumSearchLength = 3,
|
||||
searchDebouncePeriodMillis = UserListPresenterArgs.DEFAULT_DEBOUNCE
|
||||
),
|
||||
userListDataSource,
|
||||
userRepository,
|
||||
dataStore.selectedUserListDataStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -30,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
|||
get() = sequenceOf(
|
||||
aUserListState(),
|
||||
aUserListState().copy(
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = false,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
),
|
||||
aUserListState().copy(
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = true,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
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.fillMaxWidth
|
||||
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.sp
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.components.UserListView
|
||||
import io.element.android.features.createroom.impl.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.preview.ElementPreviewDark
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AddPeopleView(
|
||||
state: UserListState,
|
||||
|
|
@ -49,6 +52,7 @@ fun AddPeopleView(
|
|||
onNextPressed: () -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
if (!state.isSearchActive) {
|
||||
AddPeopleViewTopBar(
|
||||
|
|
@ -60,12 +64,14 @@ fun AddPeopleView(
|
|||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
) {
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.runtime.Composable
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.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.
|
||||
*/
|
||||
|
||||
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.PaddingValues
|
||||
|
|
@ -24,12 +24,13 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.userlist.api.UserListEvents
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.UserListStateProvider
|
||||
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
|
||||
@Composable
|
||||
fun UserListView(
|
||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.createroom.impl.configureroom
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ import android.net.Uri
|
|||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.RoomPrivacyOption
|
||||
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.designsystem.components.ProgressDialog
|
||||
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.TextButton
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||
import kotlinx.coroutines.launch
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun ConfigureRoomView(
|
||||
state: ConfigureRoomState,
|
||||
|
|
@ -104,7 +106,9 @@ fun ConfigureRoomView(
|
|||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -21,25 +21,24 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
import io.element.android.features.userlist.api.UserListDataStore
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.execute
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class CreateRoomRootPresenter @Inject constructor(
|
||||
private val presenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
||||
private val userRepository: UserRepository,
|
||||
private val userListDataStore: UserListDataStore,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : Presenter<CreateRoomRootState> {
|
||||
|
|
@ -48,10 +47,8 @@ class CreateRoomRootPresenter @Inject constructor(
|
|||
presenterFactory.create(
|
||||
UserListPresenterArgs(
|
||||
selectionMode = SelectionMode.Single,
|
||||
minimumSearchLength = 3,
|
||||
searchDebouncePeriodMillis = UserListPresenterArgs.DEFAULT_DEBOUNCE,
|
||||
),
|
||||
userListDataSource,
|
||||
userRepository,
|
||||
userListDataStore,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
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.matrix.api.core.RoomId
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.userId.value,
|
||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
||||
searchResults = SearchBarResultState.Results(persistentListOf(it)),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
|
|
@ -43,7 +44,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.userId.value,
|
||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
||||
searchResults = SearchBarResultState.Results(persistentListOf(it)),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -32,7 +34,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.sp
|
||||
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.designsystem.components.ProgressDialog
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CreateRoomRootView(
|
||||
state: CreateRoomRootState,
|
||||
|
|
@ -79,10 +81,11 @@ fun CreateRoomRootView(
|
|||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.userListState,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.LaunchedEffect
|
||||
|
|
@ -28,24 +28,16 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
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.designsystem.theme.components.SearchBarResultState
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class DefaultUserListPresenter @AssistedInject constructor(
|
||||
@Assisted val args: UserListPresenterArgs,
|
||||
@Assisted val userListDataSource: UserListDataSource,
|
||||
@Assisted val userRepository: UserRepository,
|
||||
@Assisted val userListDataStore: UserListDataStore,
|
||||
) : UserListPresenter {
|
||||
|
||||
|
|
@ -54,7 +46,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||
override fun create(
|
||||
args: UserListPresenterArgs,
|
||||
userListDataSource: UserListDataSource,
|
||||
userRepository: UserRepository,
|
||||
userListDataStore: UserListDataStore,
|
||||
): DefaultUserListPresenter
|
||||
}
|
||||
|
|
@ -64,24 +56,18 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults: UserSearchResultState by remember {
|
||||
mutableStateOf(UserSearchResultState.NotSearching)
|
||||
var searchResults: SearchBarResultState<ImmutableList<MatrixUser>> by remember {
|
||||
mutableStateOf(SearchBarResultState.NotSearching())
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
||||
searchResults = if (MatrixPatterns.isUserId(searchQuery)) {
|
||||
UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(searchQuery))))
|
||||
} else {
|
||||
UserSearchResultState.NotSearching
|
||||
}
|
||||
searchResults = SearchBarResultState.NotSearching()
|
||||
|
||||
// Debounce
|
||||
delay(args.searchDebouncePeriodMillis)
|
||||
|
||||
// Perform the search asynchronously
|
||||
if (searchQuery.length >= args.minimumSearchLength) {
|
||||
searchResults = performSearch(searchQuery)
|
||||
userRepository.search(searchQuery).collect {
|
||||
searchResults = when {
|
||||
it.isEmpty() -> SearchBarResultState.NoResults()
|
||||
else -> SearchBarResultState.Results(it.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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
|
||||
|
||||
|
|
@ -14,16 +14,17 @@
|
|||
* 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.usersearch.api.UserRepository
|
||||
|
||||
interface UserListPresenter : Presenter<UserListState> {
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
args: UserListPresenterArgs,
|
||||
userListDataSource: UserListDataSource,
|
||||
userRepository: UserRepository,
|
||||
userListDataStore: UserListDataStore,
|
||||
): UserListPresenter
|
||||
}
|
||||
|
|
@ -14,18 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userlist.api
|
||||
package io.element.android.features.createroom.impl.userlist
|
||||
|
||||
data class UserListPresenterArgs(
|
||||
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 {
|
||||
Single,
|
||||
|
|
@ -14,14 +14,15 @@
|
|||
* 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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class UserListState(
|
||||
val searchQuery: String,
|
||||
val searchResults: UserSearchResultState,
|
||||
val searchResults: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val selectionMode: SelectionMode,
|
||||
|
|
@ -29,14 +30,3 @@ data class UserListState(
|
|||
) {
|
||||
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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.userlist.api
|
||||
package io.element.android.features.createroom.impl.userlist
|
||||
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -37,19 +38,19 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||
),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "something-with-no-results",
|
||||
searchResults = UserSearchResultState.NoResults
|
||||
searchResults = SearchBarResultState.NoResults()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -57,7 +58,7 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
fun aUserListState() = UserListState(
|
||||
isSearchActive = false,
|
||||
searchQuery = "",
|
||||
searchResults = UserSearchResultState.NotSearching,
|
||||
searchResults = SearchBarResultState.NotSearching(),
|
||||
selectedUsers = persistentListOf(),
|
||||
selectionMode = SelectionMode.Single,
|
||||
eventSink = {}
|
||||
|
|
@ -21,9 +21,9 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||
import io.element.android.features.userlist.api.UserListDataStore
|
||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
|
@ -34,7 +34,11 @@ class AddPeoplePresenterTests {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore(UserListDataStore()))
|
||||
presenter = AddPeoplePresenter(
|
||||
FakeUserListPresenterFactory(),
|
||||
FakeUserRepository(),
|
||||
CreateRoomDataStore(UserListDataStore())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -42,6 +46,7 @@ class AddPeoplePresenterTests {
|
|||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// TODO This doesn't actually test anything...
|
||||
val initialState = awaitItem()
|
||||
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.CreateRoomDataStore
|
||||
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.matrix.api.core.RoomId
|
||||
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.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.userlist.api.UserListDataStore
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
|
@ -39,7 +39,7 @@ import org.junit.Test
|
|||
|
||||
class CreateRoomRootPresenterTests {
|
||||
|
||||
private lateinit var userListDataSource: FakeUserListDataSource
|
||||
private lateinit var userRepository: FakeUserRepository
|
||||
private lateinit var presenter: CreateRoomRootPresenter
|
||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||
|
|
@ -48,8 +48,8 @@ class CreateRoomRootPresenterTests {
|
|||
fun setup() {
|
||||
fakeUserListPresenter = FakeUserListPresenter()
|
||||
fakeMatrixClient = FakeMatrixClient()
|
||||
userListDataSource = FakeUserListDataSource()
|
||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, UserListDataStore(), fakeMatrixClient)
|
||||
userRepository = FakeUserRepository()
|
||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userRepository, UserListDataStore(), fakeMatrixClient)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -14,77 +14,78 @@
|
|||
* 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.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
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.designsystem.theme.components.SearchBarResultState
|
||||
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.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultUserListPresenterTests {
|
||||
|
||||
private val userListDataSource = FakeUserListDataSource()
|
||||
private val userRepository = FakeUserRepository()
|
||||
|
||||
@Test
|
||||
fun `present - initial state for single selection`() = runTest {
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource,
|
||||
UserListDataStore(),
|
||||
)
|
||||
val presenter =
|
||||
DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userRepository,
|
||||
UserListDataStore(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.searchQuery).isEmpty()
|
||||
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state for multiple selection`() = runTest {
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||
userListDataSource,
|
||||
UserListDataStore(),
|
||||
)
|
||||
val presenter =
|
||||
DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||
userRepository,
|
||||
UserListDataStore(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.searchQuery).isEmpty()
|
||||
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - update search query`() = runTest {
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource,
|
||||
UserListDataStore(),
|
||||
)
|
||||
val presenter =
|
||||
DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userRepository,
|
||||
UserListDataStore(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
||||
|
|
@ -93,12 +94,14 @@ class DefaultUserListPresenterTests {
|
|||
val matrixIdQuery = "@name:matrix.org"
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(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"
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NoResults)
|
||||
assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery)
|
||||
skipItems(1)
|
||||
|
||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||
assertThat(awaitItem().isSearchActive).isFalse()
|
||||
|
|
@ -106,39 +109,83 @@ class DefaultUserListPresenterTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - searches when minimum length exceeded`() = runTest {
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single, minimumSearchLength = 3),
|
||||
userListDataSource,
|
||||
UserListDataStore(),
|
||||
)
|
||||
fun `present - presents search results`() = runTest {
|
||||
val presenter =
|
||||
DefaultUserListPresenter(
|
||||
UserListPresenterArgs(
|
||||
selectionMode = SelectionMode.Single,
|
||||
),
|
||||
userRepository,
|
||||
UserListDataStore(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
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"))
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(aMatrixUser())))
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
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
|
||||
fun `present - select a user`() = runTest {
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource,
|
||||
UserListDataStore(),
|
||||
)
|
||||
val presenter =
|
||||
DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userRepository,
|
||||
UserListDataStore(),
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
|
||||
val userA = aMatrixUser("@userA:domain", "A")
|
||||
|
|
@ -14,12 +14,9 @@
|
|||
* 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 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 {
|
||||
|
||||
|
|
@ -14,12 +14,9 @@
|
|||
* 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.features.userlist.api.UserListDataStore
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
|
||||
class FakeUserListPresenterFactory(
|
||||
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
||||
|
|
@ -27,7 +24,7 @@ class FakeUserListPresenterFactory(
|
|||
|
||||
override fun create(
|
||||
args: UserListPresenterArgs,
|
||||
userListDataSource: UserListDataSource,
|
||||
userRepository: UserRepository,
|
||||
userListDataStore: UserListDataStore,
|
||||
): UserListPresenter = fakeUserListPresenter
|
||||
}
|
||||
|
|
@ -17,7 +17,9 @@
|
|||
package io.element.android.features.invitelist.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -103,7 +105,7 @@ fun InviteListView(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun InviteListContent(
|
||||
state: InviteListState,
|
||||
|
|
@ -124,7 +126,9 @@ fun InviteListContent(
|
|||
},
|
||||
content = { padding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(padding)
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
if (state.inviteList.isEmpty()) {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ChangeServerView(
|
||||
state: ChangeServerState,
|
||||
|
|
@ -121,6 +123,7 @@ fun ChangeServerView(
|
|||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun LoginRootView(
|
||||
state: LoginRootState,
|
||||
|
|
@ -113,6 +115,7 @@ fun LoginRootView(
|
|||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ package io.element.android.features.messages.impl
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
|
|
@ -79,7 +81,7 @@ import io.element.android.libraries.designsystem.utils.LogCompositions
|
|||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun MessagesView(
|
||||
state: MessagesState,
|
||||
|
|
@ -183,7 +185,9 @@ fun MessagesView(
|
|||
content = { padding ->
|
||||
MessagesViewContent(
|
||||
state = state,
|
||||
modifier = Modifier.padding(padding),
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
onMessageClicked = ::onMessageClicked,
|
||||
onMessageLongClicked = ::onMessageLongClicked
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
api(projects.features.roomdetails.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
@ -50,7 +51,6 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
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.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||
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.RoomMemberMainActionsSection
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RoomDetailsView(
|
||||
state: RoomDetailsState,
|
||||
|
|
@ -91,6 +93,7 @@ fun RoomDetailsView(
|
|||
) { padding ->
|
||||
Column(modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (state.roomType) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.outlined.Block
|
||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue
|
|||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -41,7 +42,7 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults by remember {
|
||||
mutableStateOf<RoomMemberSearchResultState>(RoomMemberSearchResultState.NotSearching)
|
||||
mutableStateOf<SearchBarResultState<RoomMembers>>(SearchBarResultState.NotSearching())
|
||||
}
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -60,11 +61,11 @@ class RoomMemberListPresenter @Inject constructor(
|
|||
LaunchedEffect(searchQuery) {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
searchResults = if (searchQuery.isEmpty()) {
|
||||
RoomMemberSearchResultState.NotSearching
|
||||
SearchBarResultState.NotSearching()
|
||||
} else {
|
||||
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
||||
if (results.isEmpty()) RoomMemberSearchResultState.NoResults
|
||||
else RoomMemberSearchResultState.Results(
|
||||
if (results.isEmpty()) SearchBarResultState.NoResults()
|
||||
else SearchBarResultState.Results(
|
||||
RoomMembers(
|
||||
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomMemberListState(
|
||||
val roomMembers: Async<RoomMembers>,
|
||||
val searchQuery: String,
|
||||
val searchResults: RoomMemberSearchResultState,
|
||||
val searchResults: SearchBarResultState<RoomMembers>,
|
||||
val isSearchActive: Boolean,
|
||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -32,14 +33,3 @@ data class RoomMembers(
|
|||
val invited: 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 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.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||
override val values: Sequence<RoomMemberListState>
|
||||
|
|
@ -42,7 +42,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
|||
aRoomMemberListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
searchResults = RoomMemberSearchResultState.Results(
|
||||
searchResults = SearchBarResultState.Results(
|
||||
RoomMembers(
|
||||
invited = persistentListOf(aVictor()),
|
||||
joined = persistentListOf(anAlice()),
|
||||
|
|
@ -52,14 +52,14 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
|||
aRoomMemberListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "something-with-no-results",
|
||||
searchResults = RoomMemberSearchResultState.NoResults
|
||||
searchResults = SearchBarResultState.NoResults()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomMemberListState(
|
||||
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||
searchResults: RoomMemberSearchResultState = RoomMemberSearchResultState.NotSearching,
|
||||
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.NotSearching(),
|
||||
) = RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = "",
|
||||
|
|
|
|||
|
|
@ -20,27 +20,20 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.stringResource
|
||||
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.theme.components.CenterAlignedTopAppBar
|
||||
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.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
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.room.RoomMember
|
||||
|
|
@ -71,6 +63,7 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RoomMemberListView(
|
||||
state: RoomMemberListState,
|
||||
|
|
@ -93,21 +86,20 @@ fun RoomMemberListView(
|
|||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Column {
|
||||
RoomMemberSearchBar(
|
||||
query = state.searchQuery,
|
||||
state = state.searchResults,
|
||||
active = state.isSearchActive,
|
||||
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
||||
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
||||
onUserSelected = ::onUserSelected,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
RoomMemberSearchBar(
|
||||
query = state.searchQuery,
|
||||
state = state.searchResults,
|
||||
active = state.isSearchActive,
|
||||
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
||||
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
||||
onUserSelected = ::onUserSelected,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
if (state.roomMembers is Async.Success) {
|
||||
|
|
@ -216,11 +208,10 @@ private fun RoomMemberListTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomMemberSearchBar(
|
||||
query: String,
|
||||
state: RoomMemberSearchResultState,
|
||||
state: SearchBarResultState<RoomMembers>,
|
||||
active: Boolean,
|
||||
placeHolderTitle: String,
|
||||
onActiveChanged: (Boolean) -> Unit,
|
||||
|
|
@ -228,72 +219,21 @@ private fun RoomMemberSearchBar(
|
|||
onUserSelected: (RoomMember) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
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)
|
||||
modifier = modifier,
|
||||
placeHolderTitle = placeHolderTitle,
|
||||
resultState = state,
|
||||
resultHandler = { results ->
|
||||
RoomMemberList(
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
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.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RoomMemberDetailsView(
|
||||
state: RoomMemberDetailsState,
|
||||
|
|
@ -74,6 +76,7 @@ fun RoomMemberDetailsView(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
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.RoomMemberListEvents
|
||||
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.aVictor
|
||||
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
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.MatrixRoomMembersState
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
|
|
@ -49,7 +49,7 @@ class RoomMemberListPresenterTests {
|
|||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
||||
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()
|
||||
|
||||
val loadedState = awaitItem()
|
||||
|
|
@ -89,7 +89,7 @@ class RoomMemberListPresenterTests {
|
|||
val searchQueryUpdatedState = awaitItem()
|
||||
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
||||
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()
|
||||
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
||||
val searchSearchResultDelivered = awaitItem()
|
||||
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.Results::class.java)
|
||||
Truth.assertThat((searchSearchResultDelivered.searchResults as RoomMemberSearchResultState.Results).results.joined.first().displayName)
|
||||
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||
Truth.assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.joined.first().displayName)
|
||||
.isEqualTo("Alice")
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -116,7 +118,7 @@ fun RoomListView(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RoomListContent(
|
||||
state: RoomListState,
|
||||
|
|
@ -190,6 +192,7 @@ fun RoomListContent(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.compose.animation.AnimatedVisibility
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
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.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -89,7 +91,7 @@ internal fun RoomListSearchResultView(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContent(
|
||||
state: RoomListState,
|
||||
|
|
@ -183,6 +185,7 @@ internal fun RoomListSearchResultContent(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
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()
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ accompanist = "0.30.1"
|
|||
test_core = "1.5.0"
|
||||
|
||||
#other
|
||||
coil = "2.3.0"
|
||||
coil = "2.4.0"
|
||||
datetime = "0.4.0"
|
||||
serialization_json = "1.5.0"
|
||||
showkase = "1.0.0-beta18"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ package io.element.android.libraries.designsystem.components.preferences
|
|||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
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.TopAppBar
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun PreferenceView(
|
||||
title: String,
|
||||
|
|
@ -69,6 +72,7 @@ fun PreferenceView(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it)
|
||||
.verticalScroll(
|
||||
state = scrollState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,74 +16,216 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.SearchBarColors
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.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.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.PreviewGroup
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
fun <T> SearchBar(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
placeHolderTitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
resultState: SearchBarResultState<T> = SearchBarResultState.NotSearching(),
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.Elevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
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(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = onSearch,
|
||||
onSearch = { focusManager.clearFocus() },
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier,
|
||||
modifier = modifier.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||
enabled = enabled,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
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 = { 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,
|
||||
colors = colors,
|
||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||
tonalElevation = tonalElevation,
|
||||
windowInsets = windowInsets,
|
||||
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)
|
||||
@Composable
|
||||
internal fun SearchBarPreview() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SearchBar(
|
||||
query = "Some text",
|
||||
onQueryChange = {},
|
||||
onSearch = {},
|
||||
active = false,
|
||||
onActiveChange = {},
|
||||
content = {},
|
||||
internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
query = "search term",
|
||||
active = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Search)
|
||||
@Composable
|
||||
internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview {
|
||||
ContentToPreview(
|
||||
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.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
|
|||
|
|
@ -26,11 +26,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
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
|
||||
fun CheckableMatrixUserRow(
|
||||
|
|
@ -40,18 +43,39 @@ fun CheckableMatrixUserRow(
|
|||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
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(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(role = Role.Checkbox) { onCheckedChange(!checked) },
|
||||
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MatrixUserRow(
|
||||
UserRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
matrixUser = matrixUser,
|
||||
avatarSize = avatarSize,
|
||||
avatarData = avatarData,
|
||||
name = name,
|
||||
subtext = subtext,
|
||||
)
|
||||
|
||||
Checkbox(
|
||||
|
|
@ -77,5 +101,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
|||
Column {
|
||||
CheckableMatrixUserRow(checked = true, 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.sp
|
||||
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.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -46,6 +47,19 @@ fun MatrixUserRow(
|
|||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
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(
|
||||
modifier = modifier
|
||||
|
|
@ -54,9 +68,7 @@ fun MatrixUserRow(
|
|||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
matrixUser.getAvatarData(size = avatarSize),
|
||||
)
|
||||
Avatar(avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp),
|
||||
|
|
@ -65,15 +77,15 @@ fun MatrixUserRow(
|
|||
Text(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
text = matrixUser.getBestName(),
|
||||
text = name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
// Id
|
||||
if (matrixUser.displayName.isNullOrEmpty().not()) {
|
||||
subtext?.let {
|
||||
Text(
|
||||
text = matrixUser.userId.value,
|
||||
text = subtext,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.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.Text
|
||||
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.getBestName
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun SelectedUser(
|
||||
|
|
@ -74,7 +73,7 @@ fun SelectedUser(
|
|||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(id = R.string.action_remove),
|
||||
contentDescription = stringResource(id = StringR.string.action_remove),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.PaddingValues
|
||||
|
|
@ -30,11 +30,11 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.ElementPreviewLight
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun SelectedUsersList(
|
||||
|
|
@ -82,6 +82,6 @@ internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPrev
|
|||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SelectedUsersList(
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
|
@ -87,31 +87,29 @@
|
|||
<string name="common_report_a_bug">"Report a bug"</string>
|
||||
<string name="common_report_submitted">"Report submitted"</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_select_your_server">"Select your server"</string>
|
||||
<string name="common_sending">"Sending…"</string>
|
||||
<string name="common_server_not_supported">"Server not supported"</string>
|
||||
<string name="common_server_url">"Server URL"</string>
|
||||
<string name="common_settings">"Settings"</string>
|
||||
<string name="common_starting_chat">"Starting chat…"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Success"</string>
|
||||
<string name="common_suggestions">"Suggestions"</string>
|
||||
<string name="common_topic">"Topic"</string>
|
||||
<string name="common_unable_to_decrypt">"Unable to decrypt"</string>
|
||||
<string name="common_unable_to_invite_message">"We were unable to successfully send invites to one or more users."</string>
|
||||
<string name="common_unable_to_invite_title">"Unable to send invite(s)"</string>
|
||||
<string name="common_unsupported_event">"Unsupported event"</string>
|
||||
<string name="common_username">"Username"</string>
|
||||
<string name="common_verification_cancelled">"Verification cancelled"</string>
|
||||
<string name="common_verification_complete">"Verification complete"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Waiting…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmation"</string>
|
||||
<string name="dialog_title_warning">"Warning"</string>
|
||||
<string name="emoji_picker_category_activity">"Activities"</string>
|
||||
<string name="emoji_picker_category_flags">"Flags"</string>
|
||||
<string name="common_sending">"Sending…"</string>
|
||||
<string name="common_server_not_supported">"Server not supported"</string>
|
||||
<string name="common_server_url">"Server URL"</string>
|
||||
<string name="common_settings">"Settings"</string>
|
||||
<string name="common_starting_chat">"Starting chat…"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Success"</string>
|
||||
<string name="common_suggestions">"Suggestions"</string>
|
||||
<string name="common_topic">"Topic"</string>
|
||||
<string name="common_unable_to_decrypt">"Unable to decrypt"</string>
|
||||
<string name="common_unsupported_event">"Unsupported event"</string>
|
||||
<string name="common_username">"Username"</string>
|
||||
<string name="common_verification_cancelled">"Verification cancelled"</string>
|
||||
<string name="common_verification_complete">"Verification complete"</string>
|
||||
<string name="common_video">"Video"</string>
|
||||
<string name="common_waiting">"Waiting…"</string>
|
||||
<string name="dialog_title_confirmation">"Confirmation"</string>
|
||||
<string name="dialog_title_warning">"Warning"</string>
|
||||
<string name="emoji_picker_category_activity">"Activities"</string>
|
||||
<string name="emoji_picker_category_flags">"Flags"</string>
|
||||
<string name="emoji_picker_category_foods">"Food & Drink"</string>
|
||||
<string name="emoji_picker_category_nature">"Animals & Nature"</string>
|
||||
<string name="emoji_picker_category_objects">"Objects"</string>
|
||||
|
|
|
|||
|
|
@ -13,20 +13,16 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.userlist.api"
|
||||
namespace = "io.element.android.libraries.usersearch.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* 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.user.MatrixUser
|
||||
|
|
@ -14,22 +14,12 @@
|
|||
* 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 dagger.Binds
|
||||
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
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
interface CreateRoomModule {
|
||||
|
||||
@Binds
|
||||
@Named("AllUsers")
|
||||
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): UserListDataSource
|
||||
interface UserRepository {
|
||||
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -15,13 +15,12 @@
|
|||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.userlist.impl"
|
||||
namespace = "io.element.android.libraries.usersearch.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
|
@ -29,28 +28,18 @@ anvil {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.userlist.api)
|
||||
ksp(libs.showkase.processor)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
testImplementation(projects.libraries.usersearch.test)
|
||||
}
|
||||
|
|
@ -14,15 +14,18 @@
|
|||
* 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.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
class AllMatrixUsersDataSource @Inject constructor(
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class MatrixUserListDataSource @Inject constructor(
|
||||
private val client: MatrixClient
|
||||
) : UserListDataSource {
|
||||
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.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl
|
||||
package io.element.android.libraries.usersearch.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
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 org.junit.Test
|
||||
|
||||
internal class AllMatrixUsersDataSourceTest {
|
||||
internal class MatrixUserListDataSourceTest {
|
||||
|
||||
@Test
|
||||
fun `search - returns users on success`() = runTest {
|
||||
|
|
@ -37,15 +37,21 @@ internal class AllMatrixUsersDataSourceTest {
|
|||
searchTerm = "test",
|
||||
result = Result.success(
|
||||
MatrixSearchUserResults(
|
||||
results = listOf(aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2)),
|
||||
results = listOf(
|
||||
aMatrixUserProfile(),
|
||||
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||
),
|
||||
limited = false
|
||||
)
|
||||
)
|
||||
)
|
||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
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
|
||||
|
|
@ -55,7 +61,7 @@ internal class AllMatrixUsersDataSourceTest {
|
|||
searchTerm = "test",
|
||||
result = Result.failure(Throwable("Ruhroh"))
|
||||
)
|
||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val results = dataSource.search("test")
|
||||
Truth.assertThat(results).isEmpty()
|
||||
|
|
@ -68,7 +74,7 @@ internal class AllMatrixUsersDataSourceTest {
|
|||
userId = A_USER_ID,
|
||||
result = Result.success(aMatrixUserProfile())
|
||||
)
|
||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val result = dataSource.getProfile(A_USER_ID)
|
||||
Truth.assertThat(result).isEqualTo(aMatrixUserProfile())
|
||||
|
|
@ -81,7 +87,7 @@ internal class AllMatrixUsersDataSourceTest {
|
|||
userId = A_USER_ID,
|
||||
result = Result.failure(Throwable("Ruhroh"))
|
||||
)
|
||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
||||
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||
|
||||
val result = dataSource.getProfile(A_USER_ID)
|
||||
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");
|
||||
* 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
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.userlist.test"
|
||||
namespace = "io.element.android.libraries.usersearch"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.userlist.api)
|
||||
api(libs.coroutines.core)
|
||||
api(projects.libraries.usersearch.api)
|
||||
}
|
||||
|
|
@ -14,11 +14,11 @@
|
|||
* 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.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.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:mediapickers:impl"))
|
||||
implementation(project(":libraries:mediaupload:impl"))
|
||||
implementation(project(":libraries:usersearch:impl"))
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.allServicesImpl() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cbfbfa08085539bbfa4f20bc5d149bf0db0036169a32747c96f956d0d0f4b42d
|
||||
size 93741
|
||||
oid sha256:b7787897d8dda469f17da7151260f26b6dcc5500f93ce44fb317af42ae8d457c
|
||||
size 93739
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e0ba60e784a0dc582826a3f5679a6015531dc9aca22617ec1e0dede38b97f30
|
||||
size 93678
|
||||
oid sha256:6691512398d2c7e319d15397120054f1c2aae452ffed0be47df72ecb906ac4fa
|
||||
size 93675
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:766b2d44b51a36d1b15d8a39434ce5237f8427d1150f9b4c1255043fc1e4bb79
|
||||
size 492165
|
||||
oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81
|
||||
size 492167
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:766b2d44b51a36d1b15d8a39434ce5237f8427d1150f9b4c1255043fc1e4bb79
|
||||
size 492165
|
||||
oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81
|
||||
size 492167
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08
|
||||
size 3330
|
||||
oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce
|
||||
size 3332
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f
|
||||
size 4000
|
||||
oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f
|
||||
size 4001
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08
|
||||
size 3330
|
||||
oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce
|
||||
size 3332
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f
|
||||
size 4000
|
||||
oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f
|
||||
size 4001
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1
|
||||
size 3142
|
||||
oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8
|
||||
size 3138
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67
|
||||
size 3834
|
||||
oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71
|
||||
size 3829
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1
|
||||
size 3142
|
||||
oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8
|
||||
size 3138
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67
|
||||
size 3834
|
||||
oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71
|
||||
size 3829
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6cab21ee595c96700e4577554f9c98fdc56a59bf5093aa9d81a54d10381ba122
|
||||
size 6065
|
||||
oid sha256:25d0334932490997d432115c550c42e74e7226399ef82031430bf86ba33972bd
|
||||
size 6066
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b9235621126fe90a51d557df616878d53120dada814830fba006b54f1556e69e
|
||||
size 5895
|
||||
oid sha256:a2d15387248308e5234c84b9b2b282e736f05f0495c80807bacfb0acff31eb2b
|
||||
size 5890
|
||||
|
|
|
|||
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