Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
2c3fb8a549
212 changed files with 1616 additions and 916 deletions
1
changelog.d/122.feature
Normal file
1
changelog.d/122.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Create and join rooms] Select a media from the camera
|
||||||
1
changelog.d/123.feature
Normal file
1
changelog.d/123.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Create and join rooms] Select a media from the gallery
|
||||||
|
|
@ -45,19 +45,23 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.features.userlist.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
|
implementation(projects.libraries.mediaupload.api)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(projects.libraries.usersearch.impl)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
implementation(libs.coil.compose) // FIXME temp
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.mockk)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.impl)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.features.userlist.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
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.configureroom.RoomPrivacy
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -24,7 +25,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
data class CreateRoomConfig(
|
data class CreateRoomConfig(
|
||||||
val roomName: String? = null,
|
val roomName: String? = null,
|
||||||
val topic: String? = null,
|
val topic: String? = null,
|
||||||
val avatarUrl: String? = null,
|
val avatarUri: Uri? = null,
|
||||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
val privacy: RoomPrivacy? = null,
|
val privacy: RoomPrivacy = RoomPrivacy.Private,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,16 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
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.configureroom.RoomPrivacy
|
||||||
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SingleIn(CreateRoomScope::class)
|
@SingleIn(CreateRoomScope::class)
|
||||||
|
|
@ -32,6 +34,11 @@ class CreateRoomDataStore @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
|
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
|
||||||
|
private var cachedAvatarUri: Uri? = null
|
||||||
|
set(value) {
|
||||||
|
field?.path?.let { File(it) }?.delete()
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
fun getCreateRoomConfig(): Flow<CreateRoomConfig> = combine(
|
fun getCreateRoomConfig(): Flow<CreateRoomConfig> = combine(
|
||||||
selectedUserListDataStore.selectedUsers(),
|
selectedUserListDataStore.selectedUsers(),
|
||||||
|
|
@ -48,11 +55,16 @@ class CreateRoomDataStore @Inject constructor(
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() }))
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAvatarUrl(avatarUrl: String?) {
|
fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUrl = avatarUrl))
|
cachedAvatarUri = uri.takeIf { cached }
|
||||||
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUri = uri))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPrivacy(privacy: RoomPrivacy?) {
|
fun setPrivacy(privacy: RoomPrivacy) {
|
||||||
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy))
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCachedData() {
|
||||||
|
cachedAvatarUri = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,17 @@ package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
class AddPeoplePresenter @Inject constructor(
|
class AddPeoplePresenter @Inject constructor(
|
||||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||||
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
private val userRepository: UserRepository,
|
||||||
private val dataStore: CreateRoomDataStore,
|
private val dataStore: CreateRoomDataStore,
|
||||||
) : Presenter<UserListState> {
|
) : Presenter<UserListState> {
|
||||||
|
|
||||||
|
|
@ -37,10 +36,8 @@ class AddPeoplePresenter @Inject constructor(
|
||||||
userListPresenterFactory.create(
|
userListPresenterFactory.create(
|
||||||
UserListPresenterArgs(
|
UserListPresenterArgs(
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
minimumSearchLength = 3,
|
|
||||||
searchDebouncePeriodMillis = UserListPresenterArgs.DEFAULT_DEBOUNCE
|
|
||||||
),
|
),
|
||||||
userListDataSource,
|
userRepository,
|
||||||
dataStore.selectedUserListDataStore,
|
dataStore.selectedUserListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
|
@ -30,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aUserListState(),
|
aUserListState(),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -30,8 +32,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.components.UserListView
|
||||||
import io.element.android.features.userlist.api.components.UserListView
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -41,7 +43,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddPeopleView(
|
fun AddPeopleView(
|
||||||
state: UserListState,
|
state: UserListState,
|
||||||
|
|
@ -50,6 +52,7 @@ fun AddPeopleView(
|
||||||
onNextPressed: () -> Unit = {},
|
onNextPressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
AddPeopleViewTopBar(
|
AddPeopleViewTopBar(
|
||||||
|
|
@ -61,12 +64,14 @@ fun AddPeopleView(
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
) {
|
) {
|
||||||
UserListView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ fun LabelledTextField(
|
||||||
value = value,
|
value = value,
|
||||||
placeholder = { Text(placeholder) },
|
placeholder = { Text(placeholder) },
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
|
singleLine = maxLines == 1,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchUserBar(
|
||||||
|
query: String,
|
||||||
|
state: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||||
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
|
active: Boolean,
|
||||||
|
isMultiSelectionEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
||||||
|
onActiveChanged: (Boolean) -> Unit = {},
|
||||||
|
onTextChanged: (String) -> Unit = {},
|
||||||
|
onUserSelected: (MatrixUser) -> Unit = {},
|
||||||
|
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onTextChanged,
|
||||||
|
active = active,
|
||||||
|
onActiveChange = onActiveChanged,
|
||||||
|
modifier = modifier,
|
||||||
|
placeHolderTitle = placeHolderTitle,
|
||||||
|
contentPrefix = {
|
||||||
|
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
||||||
|
SelectedUsersList(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
selectedUsers = selectedUsers,
|
||||||
|
autoScroll = true,
|
||||||
|
onUserRemoved = onUserDeselected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resultState = state,
|
||||||
|
resultHandler = { users ->
|
||||||
|
LazyColumn {
|
||||||
|
if (isMultiSelectionEnabled) {
|
||||||
|
items(users) { matrixUser ->
|
||||||
|
SearchMultipleUsersResultItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (checked) {
|
||||||
|
onUserSelected(matrixUser)
|
||||||
|
} else {
|
||||||
|
onUserDeselected(matrixUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(users) { matrixUser ->
|
||||||
|
SearchSingleUserResultItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
onClick = { onUserSelected(matrixUser) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.features.createroom.impl.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -24,12 +24,13 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.userlist.api.UserListStateProvider
|
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserListView(
|
fun UserListView(
|
||||||
|
|
@ -16,16 +16,16 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface ConfigureRoomEvents {
|
sealed interface ConfigureRoomEvents {
|
||||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||||
data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents
|
data class RoomPrivacyChanged(val privacy: RoomPrivacy) : ConfigureRoomEvents
|
||||||
data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents
|
|
||||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||||
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
||||||
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||||
object CancelCreateRoom : ConfigureRoomEvents
|
object CancelCreateRoom : ConfigureRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
@ -26,14 +27,19 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.execute
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||||
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||||
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
|
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
|
||||||
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -41,14 +47,28 @@ import javax.inject.Inject
|
||||||
class ConfigureRoomPresenter @Inject constructor(
|
class ConfigureRoomPresenter @Inject constructor(
|
||||||
private val dataStore: CreateRoomDataStore,
|
private val dataStore: CreateRoomDataStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val mediaPickerProvider: PickerProvider,
|
||||||
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfigureRoomState {
|
override fun present(): ConfigureRoomState {
|
||||||
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig())
|
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig())
|
||||||
val isCreateButtonEnabled by remember(createRoomConfig.value.roomName, createRoomConfig.value.privacy) {
|
|
||||||
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
||||||
|
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
|
||||||
|
)
|
||||||
|
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
|
||||||
|
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) }
|
||||||
|
)
|
||||||
|
|
||||||
|
val avatarActions by remember(createRoomConfig.value.avatarUri) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
createRoomConfig.value.roomName.isNullOrEmpty().not() && createRoomConfig.value.privacy != null
|
listOfNotNull(
|
||||||
|
AvatarAction.TakePhoto,
|
||||||
|
AvatarAction.ChoosePhoto,
|
||||||
|
AvatarAction.Remove.takeIf { createRoomConfig.value.avatarUri != null },
|
||||||
|
).toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,26 +82,37 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
|
|
||||||
fun handleEvents(event: ConfigureRoomEvents) {
|
fun handleEvents(event: ConfigureRoomEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString())
|
|
||||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||||
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
|
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
|
||||||
is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
|
is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
|
||||||
is ConfigureRoomEvents.CreateRoom -> createRoom(event.config)
|
is ConfigureRoomEvents.CreateRoom -> createRoom(event.config)
|
||||||
|
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||||
|
when (event.action) {
|
||||||
|
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||||
|
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
|
||||||
|
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = Async.Uninitialized
|
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = Async.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigureRoomState(
|
return ConfigureRoomState(
|
||||||
config = createRoomConfig.value,
|
config = createRoomConfig.value,
|
||||||
isCreateButtonEnabled = isCreateButtonEnabled,
|
avatarActions = avatarActions,
|
||||||
createRoomAction = createRoomAction.value,
|
createRoomAction = createRoomAction.value,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.createRoom(config: CreateRoomConfig, createRoomAction: MutableState<Async<RoomId>>) = launch {
|
private fun CoroutineScope.createRoom(
|
||||||
|
config: CreateRoomConfig,
|
||||||
|
createRoomAction: MutableState<Async<RoomId>>
|
||||||
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
|
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
|
||||||
val params = CreateRoomParameters(
|
val params = CreateRoomParameters(
|
||||||
name = config.roomName,
|
name = config.roomName,
|
||||||
topic = config.topic,
|
topic = config.topic,
|
||||||
|
|
@ -90,9 +121,16 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE,
|
visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE,
|
||||||
preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT,
|
preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT,
|
||||||
invite = config.invites.map { it.userId },
|
invite = config.invites.map { it.userId },
|
||||||
avatar = config.avatarUrl,
|
avatar = avatarUrl,
|
||||||
)
|
)
|
||||||
matrixClient.createRoom(params).getOrThrow()
|
matrixClient.createRoom(params).getOrThrow()
|
||||||
|
.also { dataStore.clearCachedData() }
|
||||||
}.execute(createRoomAction)
|
}.execute(createRoomAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun uploadAvatar(avatarUri: Uri): String {
|
||||||
|
val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow()
|
||||||
|
val byteArray = preprocessed.file.readBytes()
|
||||||
|
return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray).getOrThrow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,16 @@
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
data class ConfigureRoomState(
|
||||||
val config: CreateRoomConfig,
|
val config: CreateRoomConfig,
|
||||||
val isCreateButtonEnabled: Boolean,
|
val avatarActions: ImmutableList<AvatarAction>,
|
||||||
val createRoomAction: Async<RoomId>,
|
val createRoomAction: Async<RoomId>,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
)
|
) {
|
||||||
|
val isCreateButtonEnabled: Boolean = config.roomName.isNullOrEmpty().not()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
override val values: Sequence<ConfigureRoomState>
|
override val values: Sequence<ConfigureRoomState>
|
||||||
|
|
@ -30,16 +31,15 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
||||||
roomName = "Room 101",
|
roomName = "Room 101",
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
invites = aListOfSelectedUsers(),
|
invites = aListOfSelectedUsers(),
|
||||||
privacy = RoomPrivacy.Private,
|
privacy = RoomPrivacy.Public,
|
||||||
),
|
),
|
||||||
isCreateButtonEnabled = true,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aConfigureRoomState() = ConfigureRoomState(
|
fun aConfigureRoomState() = ConfigureRoomState(
|
||||||
config = CreateRoomConfig(),
|
config = CreateRoomConfig(),
|
||||||
isCreateButtonEnabled = false,
|
avatarActions = persistentListOf(),
|
||||||
createRoomAction = Async.Uninitialized,
|
createRoomAction = Async.Uninitialized,
|
||||||
eventSink = {}
|
eventSink = { },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,37 +14,42 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
|
import androidx.compose.material.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.focus.FocusManager
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.net.toUri
|
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.createroom.impl.components.Avatar
|
import io.element.android.features.createroom.impl.components.Avatar
|
||||||
import io.element.android.features.createroom.impl.components.LabelledTextField
|
import io.element.android.features.createroom.impl.components.LabelledTextField
|
||||||
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
||||||
import io.element.android.features.userlist.api.components.SelectedUsersList
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
|
@ -56,8 +61,11 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigureRoomView(
|
fun ConfigureRoomView(
|
||||||
state: ConfigureRoomState,
|
state: ConfigureRoomState,
|
||||||
|
|
@ -65,59 +73,96 @@ fun ConfigureRoomView(
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onRoomCreated: (RoomId) -> Unit = {},
|
onRoomCreated: (RoomId) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
||||||
|
initialValue = ModalBottomSheetValue.Hidden,
|
||||||
|
)
|
||||||
|
|
||||||
if (state.createRoomAction is Async.Success) {
|
if (state.createRoomAction is Async.Success) {
|
||||||
LaunchedEffect(state.createRoomAction) {
|
LaunchedEffect(state.createRoomAction) {
|
||||||
onRoomCreated(state.createRoomAction.state)
|
onRoomCreated(state.createRoomAction.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
fun onAvatarClicked() {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
coroutineScope.launch {
|
||||||
|
itemActionsBottomSheetState.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier.clearFocusOnTap(focusManager),
|
||||||
topBar = {
|
topBar = {
|
||||||
ConfigureRoomToolbar(
|
ConfigureRoomToolbar(
|
||||||
isNextActionEnabled = state.isCreateButtonEnabled,
|
isNextActionEnabled = state.isCreateButtonEnabled,
|
||||||
onBackPressed = onBackPressed,
|
onBackPressed = onBackPressed,
|
||||||
onNextPressed = {
|
onNextPressed = {
|
||||||
|
focusManager.clearFocus()
|
||||||
state.eventSink(ConfigureRoomEvents.CreateRoom(state.config))
|
state.eventSink(ConfigureRoomEvents.CreateRoom(state.config))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
) {
|
) {
|
||||||
RoomNameWithAvatar(
|
item {
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
RoomNameWithAvatar(
|
||||||
avatarUri = state.config.avatarUrl?.toUri(),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
roomName = state.config.roomName.orEmpty(),
|
avatarUri = state.config.avatarUri,
|
||||||
onAvatarClick = { Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() },
|
roomName = state.config.roomName.orEmpty(),
|
||||||
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
onAvatarClick = ::onAvatarClicked,
|
||||||
)
|
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
||||||
RoomTopic(
|
)
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
}
|
||||||
topic = state.config.topic.orEmpty(),
|
item {
|
||||||
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
RoomTopic(
|
||||||
)
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
SelectedUsersList(
|
topic = state.config.topic.orEmpty(),
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp),
|
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||||
selectedUsers = state.config.invites,
|
)
|
||||||
onUserRemoved = { state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it)) },
|
}
|
||||||
)
|
if (state.config.invites.isNotEmpty()) {
|
||||||
Spacer(Modifier.weight(1f))
|
item {
|
||||||
RoomPrivacyOptions(
|
SelectedUsersList(
|
||||||
modifier = Modifier.padding(bottom = 40.dp),
|
contentPadding = PaddingValues(horizontal = 24.dp),
|
||||||
selected = state.config.privacy,
|
selectedUsers = state.config.invites,
|
||||||
onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) },
|
onUserRemoved = {
|
||||||
)
|
focusManager.clearFocus()
|
||||||
|
state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
RoomPrivacyOptions(
|
||||||
|
modifier = Modifier.padding(bottom = 40.dp),
|
||||||
|
selected = state.config.privacy,
|
||||||
|
onOptionSelected = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvatarActionListView(
|
||||||
|
actions = state.avatarActions,
|
||||||
|
modalBottomSheetState = itemActionsBottomSheetState,
|
||||||
|
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
|
||||||
|
)
|
||||||
|
|
||||||
when (state.createRoomAction) {
|
when (state.createRoomAction) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
|
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
RetryDialog(
|
RetryDialog(
|
||||||
content = stringResource(R.string.screen_create_room_error_creating_room),
|
content = stringResource(R.string.screen_create_room_error_creating_room),
|
||||||
|
|
@ -125,10 +170,12 @@ fun ConfigureRoomView(
|
||||||
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
|
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigureRoomToolbar(
|
fun ConfigureRoomToolbar(
|
||||||
isNextActionEnabled: Boolean,
|
isNextActionEnabled: Boolean,
|
||||||
|
|
@ -238,3 +285,11 @@ private fun ContentToPreview(state: ConfigureRoomState) {
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||||
|
pointerInput(Unit) {
|
||||||
|
detectTapGestures(onTap = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.configureroom.avatar
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.PhotoCamera
|
||||||
|
import androidx.compose.material.icons.outlined.PhotoLibrary
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class AvatarAction(
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val destructive: Boolean = false,
|
||||||
|
) {
|
||||||
|
object TakePhoto : AvatarAction(titleResId = R.string.action_take_photo, icon = Icons.Outlined.PhotoCamera)
|
||||||
|
object ChoosePhoto : AvatarAction(titleResId = R.string.action_choose_photo, icon = Icons.Outlined.PhotoLibrary)
|
||||||
|
object Remove : AvatarAction(titleResId = R.string.action_remove, icon = Icons.Outlined.Delete, destructive = true)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterialApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom.avatar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.ModalBottomSheetState
|
||||||
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AvatarActionListView(
|
||||||
|
actions: ImmutableList<AvatarAction>,
|
||||||
|
modalBottomSheetState: ModalBottomSheetState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionSelected: (action: AvatarAction) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
fun onItemActionClicked(itemAction: AvatarAction) {
|
||||||
|
onActionSelected(itemAction)
|
||||||
|
coroutineScope.launch {
|
||||||
|
modalBottomSheetState.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ModalBottomSheetLayout(
|
||||||
|
modifier = modifier,
|
||||||
|
sheetState = modalBottomSheetState,
|
||||||
|
sheetContent = {
|
||||||
|
SheetContent(
|
||||||
|
actions = actions,
|
||||||
|
onActionClicked = ::onItemActionClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.imePadding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SheetContent(
|
||||||
|
actions: ImmutableList<AvatarAction>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onActionClicked: (AvatarAction) -> Unit = { },
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = actions,
|
||||||
|
) { action ->
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable { onActionClicked(action) },
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(action.titleResId),
|
||||||
|
color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = action.icon,
|
||||||
|
contentDescription = stringResource(action.titleResId),
|
||||||
|
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SheetContentLightPreview() =
|
||||||
|
ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SheetContentDarkPreview() =
|
||||||
|
ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
AvatarActionListView(
|
||||||
|
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove),
|
||||||
|
modalBottomSheetState = ModalBottomSheetState(
|
||||||
|
initialValue = ModalBottomSheetValue.Expanded
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -21,25 +21,24 @@ import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.execute
|
import io.element.android.libraries.architecture.execute
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
class CreateRoomRootPresenter @Inject constructor(
|
class CreateRoomRootPresenter @Inject constructor(
|
||||||
private val presenterFactory: UserListPresenter.Factory,
|
private val presenterFactory: UserListPresenter.Factory,
|
||||||
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
private val userRepository: UserRepository,
|
||||||
private val userListDataStore: UserListDataStore,
|
private val userListDataStore: UserListDataStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
) : Presenter<CreateRoomRootState> {
|
) : Presenter<CreateRoomRootState> {
|
||||||
|
|
@ -48,10 +47,8 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
presenterFactory.create(
|
presenterFactory.create(
|
||||||
UserListPresenterArgs(
|
UserListPresenterArgs(
|
||||||
selectionMode = SelectionMode.Single,
|
selectionMode = SelectionMode.Single,
|
||||||
minimumSearchLength = 3,
|
|
||||||
searchDebouncePeriodMillis = UserListPresenterArgs.DEFAULT_DEBOUNCE,
|
|
||||||
),
|
),
|
||||||
userListDataSource,
|
userRepository,
|
||||||
userListDataStore,
|
userListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchQuery = it.userId.value,
|
searchQuery = it.userId.value,
|
||||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
searchResults = SearchBarResultState.Results(persistentListOf(it)),
|
||||||
selectedUsers = persistentListOf(it),
|
selectedUsers = persistentListOf(it),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
)
|
)
|
||||||
|
|
@ -43,7 +44,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
userListState = aMatrixUser().let {
|
userListState = aMatrixUser().let {
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
searchQuery = it.userId.value,
|
searchQuery = it.userId.value,
|
||||||
searchResults = UserSearchResultState.Results(persistentListOf(it)),
|
searchResults = SearchBarResultState.Results(persistentListOf(it)),
|
||||||
selectedUsers = persistentListOf(it),
|
selectedUsers = persistentListOf(it),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -32,7 +34,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -40,7 +41,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.userlist.api.components.UserListView
|
import io.element.android.features.createroom.impl.components.UserListView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
|
|
@ -55,7 +56,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.designsystem.R as DrawableR
|
import io.element.android.libraries.designsystem.R as DrawableR
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomRootView(
|
fun CreateRoomRootView(
|
||||||
state: CreateRoomRootState,
|
state: CreateRoomRootState,
|
||||||
|
|
@ -79,10 +80,11 @@ fun CreateRoomRootView(
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.consumeWindowInsets(paddingValues),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
UserListView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = state.userListState,
|
state = state.userListState,
|
||||||
|
|
@ -102,7 +104,7 @@ fun CreateRoomRootView(
|
||||||
|
|
||||||
when (state.startDmAction) {
|
when (state.startDmAction) {
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
ProgressDialog(text = stringResource(id = StringR.string.common_creating_room))
|
ProgressDialog(text = stringResource(id = StringR.string.common_starting_chat))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.impl
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
|
@ -28,24 +28,16 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
class DefaultUserListPresenter @AssistedInject constructor(
|
class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Assisted val args: UserListPresenterArgs,
|
@Assisted val args: UserListPresenterArgs,
|
||||||
@Assisted val userListDataSource: UserListDataSource,
|
@Assisted val userRepository: UserRepository,
|
||||||
@Assisted val userListDataStore: UserListDataStore,
|
@Assisted val userListDataStore: UserListDataStore,
|
||||||
) : UserListPresenter {
|
) : UserListPresenter {
|
||||||
|
|
||||||
|
|
@ -54,7 +46,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
interface DefaultUserListFactory : UserListPresenter.Factory {
|
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
args: UserListPresenterArgs,
|
args: UserListPresenterArgs,
|
||||||
userListDataSource: UserListDataSource,
|
userRepository: UserRepository,
|
||||||
userListDataStore: UserListDataStore,
|
userListDataStore: UserListDataStore,
|
||||||
): DefaultUserListPresenter
|
): DefaultUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
@ -64,24 +56,18 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchResults: UserSearchResultState by remember {
|
var searchResults: SearchBarResultState<ImmutableList<MatrixUser>> by remember {
|
||||||
mutableStateOf(UserSearchResultState.NotSearching)
|
mutableStateOf(SearchBarResultState.NotSearching())
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any
|
searchResults = SearchBarResultState.NotSearching()
|
||||||
searchResults = if (MatrixPatterns.isUserId(searchQuery)) {
|
|
||||||
UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(searchQuery))))
|
|
||||||
} else {
|
|
||||||
UserSearchResultState.NotSearching
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce
|
userRepository.search(searchQuery).collect {
|
||||||
delay(args.searchDebouncePeriodMillis)
|
searchResults = when {
|
||||||
|
it.isEmpty() -> SearchBarResultState.NoResults()
|
||||||
// Perform the search asynchronously
|
else -> SearchBarResultState.Results(it.toImmutableList())
|
||||||
if (searchQuery.length >= args.minimumSearchLength) {
|
}
|
||||||
searchResults = performSearch(searchQuery)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,15 +87,4 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performSearch(query: String): UserSearchResultState {
|
|
||||||
val isMatrixId = MatrixPatterns.isUserId(query)
|
|
||||||
val results = userListDataSource.search(query).toMutableList()
|
|
||||||
if (isMatrixId && results.none { it.userId.value == query }) {
|
|
||||||
val getProfileResult: MatrixUser? = userListDataSource.getProfile(UserId(query))
|
|
||||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
|
||||||
results.add(0, profile)
|
|
||||||
}
|
|
||||||
return if (results.isEmpty()) UserSearchResultState.NoResults else UserSearchResultState.Results(results.toImmutableList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
|
|
@ -14,16 +14,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
|
||||||
interface UserListPresenter : Presenter<UserListState> {
|
interface UserListPresenter : Presenter<UserListState> {
|
||||||
|
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
args: UserListPresenterArgs,
|
args: UserListPresenterArgs,
|
||||||
userListDataSource: UserListDataSource,
|
userRepository: UserRepository,
|
||||||
userListDataStore: UserListDataStore,
|
userListDataStore: UserListDataStore,
|
||||||
): UserListPresenter
|
): UserListPresenter
|
||||||
}
|
}
|
||||||
|
|
@ -14,18 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
data class UserListPresenterArgs(
|
data class UserListPresenterArgs(
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
val minimumSearchLength: Int = 1,
|
)
|
||||||
val searchDebouncePeriodMillis: Long = NO_DEBOUNCE,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val NO_DEBOUNCE = 0L
|
|
||||||
const val DEFAULT_DEBOUNCE = 500L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class SelectionMode {
|
enum class SelectionMode {
|
||||||
Single,
|
Single,
|
||||||
|
|
@ -14,14 +14,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class UserListState(
|
data class UserListState(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: UserSearchResultState,
|
val searchResults: SearchBarResultState<ImmutableList<MatrixUser>>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
|
|
@ -29,14 +30,3 @@ data class UserListState(
|
||||||
) {
|
) {
|
||||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface UserSearchResultState {
|
|
||||||
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
|
||||||
object NotSearching : UserSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, but no results were found. */
|
|
||||||
object NoResults : UserSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, and some matching users were found. */
|
|
||||||
data class Results(val results: ImmutableList<MatrixUser>) : UserSearchResultState
|
|
||||||
}
|
|
||||||
|
|
@ -14,9 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -37,19 +38,19 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = UserSearchResultState.Results(aMatrixUserList().toImmutableList()),
|
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "something-with-no-results",
|
searchQuery = "something-with-no-results",
|
||||||
searchResults = UserSearchResultState.NoResults
|
searchResults = SearchBarResultState.NoResults()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +58,7 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
fun aUserListState() = UserListState(
|
fun aUserListState() = UserListState(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
searchResults = UserSearchResultState.NotSearching,
|
searchResults = SearchBarResultState.NotSearching(),
|
||||||
selectedUsers = persistentListOf(),
|
selectedUsers = persistentListOf(),
|
||||||
selectionMode = SelectionMode.Single,
|
selectionMode = SelectionMode.Single,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
@ -21,9 +21,9 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -34,7 +34,11 @@ class AddPeoplePresenterTests {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore(UserListDataStore()))
|
presenter = AddPeoplePresenter(
|
||||||
|
FakeUserListPresenterFactory(),
|
||||||
|
FakeUserRepository(),
|
||||||
|
CreateRoomDataStore(UserListDataStore())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -42,6 +46,7 @@ class AddPeoplePresenterTests {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
// TODO This doesn't actually test anything...
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState)
|
assertThat(initialState)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
|
||||||
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
|
@ -32,29 +33,57 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkAll
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
|
||||||
|
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class ConfigureRoomPresenterTests {
|
class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
private lateinit var presenter: ConfigureRoomPresenter
|
private lateinit var presenter: ConfigureRoomPresenter
|
||||||
private lateinit var userListDataStore: UserListDataStore
|
private lateinit var userListDataStore: UserListDataStore
|
||||||
|
private lateinit var createRoomDataStore: CreateRoomDataStore
|
||||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||||
|
private lateinit var fakePickerProvider: FakePickerProvider
|
||||||
|
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
fakeMatrixClient = FakeMatrixClient()
|
fakeMatrixClient = FakeMatrixClient()
|
||||||
userListDataStore = UserListDataStore()
|
userListDataStore = UserListDataStore()
|
||||||
|
createRoomDataStore = CreateRoomDataStore(userListDataStore)
|
||||||
|
fakePickerProvider = FakePickerProvider()
|
||||||
|
fakeMediaPreProcessor = FakeMediaPreProcessor()
|
||||||
presenter = ConfigureRoomPresenter(
|
presenter = ConfigureRoomPresenter(
|
||||||
dataStore = CreateRoomDataStore(userListDataStore),
|
dataStore = createRoomDataStore,
|
||||||
matrixClient = fakeMatrixClient
|
matrixClient = fakeMatrixClient,
|
||||||
|
mediaPickerProvider = fakePickerProvider,
|
||||||
|
mediaPreProcessor = fakeMediaPreProcessor,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mockkStatic(File::readBytes)
|
||||||
|
every { any<File>().readBytes() } returns byteArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -67,8 +96,8 @@ class ConfigureRoomPresenterTests {
|
||||||
assertThat(initialState.config.roomName).isNull()
|
assertThat(initialState.config.roomName).isNull()
|
||||||
assertThat(initialState.config.topic).isNull()
|
assertThat(initialState.config.topic).isNull()
|
||||||
assertThat(initialState.config.invites).isEmpty()
|
assertThat(initialState.config.invites).isEmpty()
|
||||||
assertThat(initialState.config.avatarUrl).isNull()
|
assertThat(initialState.config.avatarUri).isNull()
|
||||||
assertThat(initialState.config.privacy).isNull()
|
assertThat(initialState.config.privacy).isEqualTo(RoomPrivacy.Private)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,13 +115,6 @@ class ConfigureRoomPresenterTests {
|
||||||
var newState: ConfigureRoomState = awaitItem()
|
var newState: ConfigureRoomState = awaitItem()
|
||||||
config = config.copy(roomName = A_ROOM_NAME)
|
config = config.copy(roomName = A_ROOM_NAME)
|
||||||
assertThat(newState.config).isEqualTo(config)
|
assertThat(newState.config).isEqualTo(config)
|
||||||
assertThat(newState.isCreateButtonEnabled).isFalse()
|
|
||||||
|
|
||||||
// Select privacy
|
|
||||||
newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private))
|
|
||||||
newState = awaitItem()
|
|
||||||
config = config.copy(privacy = RoomPrivacy.Private)
|
|
||||||
assertThat(newState.config).isEqualTo(config)
|
|
||||||
assertThat(newState.isCreateButtonEnabled).isTrue()
|
assertThat(newState.isCreateButtonEnabled).isTrue()
|
||||||
|
|
||||||
// Clear room name
|
// Clear room name
|
||||||
|
|
@ -136,10 +158,28 @@ class ConfigureRoomPresenterTests {
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room avatar
|
// Room avatar
|
||||||
val anUri = Uri.parse(AN_AVATAR_URL)
|
// Pick avatar
|
||||||
newState.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri))
|
fakePickerProvider.givenResult(null)
|
||||||
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||||
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
|
// From gallery
|
||||||
|
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
|
||||||
|
fakePickerProvider.givenResult(uriFromGallery)
|
||||||
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUrl = anUri.toString())
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
// From camera
|
||||||
|
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
|
||||||
|
fakePickerProvider.givenResult(uriFromCamera)
|
||||||
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
// Remove
|
||||||
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(avatarUri = null)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room privacy
|
// Room privacy
|
||||||
|
|
@ -174,6 +214,30 @@ class ConfigureRoomPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - trigger create room with upload error and retry`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
|
||||||
|
fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
|
||||||
|
fakeMatrixClient.givenUploadMediaResult(Result.failure(A_THROWABLE))
|
||||||
|
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||||
|
val stateAfterCreateRoom = awaitItem()
|
||||||
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java)
|
||||||
|
|
||||||
|
fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
|
||||||
|
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||||
|
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||||
|
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
|
||||||
|
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Success::class.java)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - trigger retry and cancel actions`() = runTest {
|
fun `present - trigger retry and cancel actions`() = runTest {
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,10 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenter
|
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -32,6 +31,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
@ -39,7 +39,7 @@ import org.junit.Test
|
||||||
|
|
||||||
class CreateRoomRootPresenterTests {
|
class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
private lateinit var userListDataSource: FakeUserListDataSource
|
private lateinit var userRepository: FakeUserRepository
|
||||||
private lateinit var presenter: CreateRoomRootPresenter
|
private lateinit var presenter: CreateRoomRootPresenter
|
||||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||||
|
|
@ -48,8 +48,8 @@ class CreateRoomRootPresenterTests {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
fakeUserListPresenter = FakeUserListPresenter()
|
fakeUserListPresenter = FakeUserListPresenter()
|
||||||
fakeMatrixClient = FakeMatrixClient()
|
fakeMatrixClient = FakeMatrixClient()
|
||||||
userListDataSource = FakeUserListDataSource()
|
userRepository = FakeUserRepository()
|
||||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, UserListDataStore(), fakeMatrixClient)
|
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userRepository, UserListDataStore(), fakeMatrixClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -14,77 +14,78 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.impl
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultUserListPresenterTests {
|
class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
private val userListDataSource = FakeUserListDataSource()
|
private val userRepository = FakeUserRepository()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for single selection`() = runTest {
|
fun `present - initial state for single selection`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.searchQuery).isEmpty()
|
assertThat(initialState.searchQuery).isEmpty()
|
||||||
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.selectedUsers).isEmpty()
|
assertThat(initialState.selectedUsers).isEmpty()
|
||||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for multiple selection`() = runTest {
|
fun `present - initial state for multiple selection`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.searchQuery).isEmpty()
|
assertThat(initialState.searchQuery).isEmpty()
|
||||||
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
assertThat(initialState.selectedUsers).isEmpty()
|
assertThat(initialState.selectedUsers).isEmpty()
|
||||||
assertThat(initialState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - update search query`() = runTest {
|
fun `present - update search query`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
||||||
|
|
@ -93,12 +94,14 @@ class DefaultUserListPresenterTests {
|
||||||
val matrixIdQuery = "@name:matrix.org"
|
val matrixIdQuery = "@name:matrix.org"
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(MatrixUser(UserId(matrixIdQuery)))))
|
assertThat(userRepository.providedQuery).isEqualTo(matrixIdQuery)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
val notMatrixIdQuery = "name"
|
val notMatrixIdQuery = "name"
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NoResults)
|
assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery)
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
assertThat(awaitItem().isSearchActive).isFalse()
|
||||||
|
|
@ -106,39 +109,83 @@ class DefaultUserListPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - searches when minimum length exceeded`() = runTest {
|
fun `present - presents search results`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single, minimumSearchLength = 3),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(
|
||||||
UserListDataStore(),
|
selectionMode = SelectionMode.Single,
|
||||||
)
|
),
|
||||||
|
userRepository,
|
||||||
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
// When the search term is too short, nothing happens
|
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("al"))
|
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
|
||||||
|
|
||||||
// When it reaches the minimum length, a search is performed asynchronously
|
|
||||||
userListDataSource.givenSearchResult(listOf(aMatrixUser()))
|
|
||||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
assertThat(awaitItem().searchResults).isEqualTo(UserSearchResultState.Results(persistentListOf(aMatrixUser())))
|
assertThat(userRepository.providedQuery).isEqualTo("alice")
|
||||||
|
skipItems(2)
|
||||||
|
|
||||||
|
// When the user repository emits a result, it's copied to the state
|
||||||
|
userRepository.emitResult(listOf(aMatrixUser()))
|
||||||
|
assertThat(awaitItem().searchResults).isEqualTo(
|
||||||
|
SearchBarResultState.Results(
|
||||||
|
persistentListOf(aMatrixUser())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// When the user repository emits another result, it replaces the previous value
|
||||||
|
userRepository.emitResult(aMatrixUserList())
|
||||||
|
assertThat(awaitItem().searchResults).isEqualTo(
|
||||||
|
SearchBarResultState.Results(
|
||||||
|
aMatrixUserList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - presents search results when not found`() = runTest {
|
||||||
|
val presenter =
|
||||||
|
DefaultUserListPresenter(
|
||||||
|
UserListPresenterArgs(
|
||||||
|
selectionMode = SelectionMode.Single,
|
||||||
|
),
|
||||||
|
userRepository,
|
||||||
|
UserListDataStore(),
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
|
||||||
|
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||||
|
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
|
assertThat(userRepository.providedQuery).isEqualTo("alice")
|
||||||
|
skipItems(2)
|
||||||
|
|
||||||
|
// When the results list is empty, the state is set to NoResults
|
||||||
|
userRepository.emitResult(emptyList())
|
||||||
|
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - select a user`() = runTest {
|
fun `present - select a user`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter =
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
DefaultUserListPresenter(
|
||||||
userListDataSource,
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
UserListDataStore(),
|
userRepository,
|
||||||
)
|
UserListDataStore(),
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
val userA = aMatrixUser("@userA:domain", "A")
|
val userA = aMatrixUser("@userA:domain", "A")
|
||||||
|
|
@ -14,12 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
|
||||||
|
|
||||||
class FakeUserListPresenter : UserListPresenter {
|
class FakeUserListPresenter : UserListPresenter {
|
||||||
|
|
||||||
|
|
@ -14,12 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.features.createroom.impl.userlist
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
|
|
||||||
class FakeUserListPresenterFactory(
|
class FakeUserListPresenterFactory(
|
||||||
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
||||||
|
|
@ -27,7 +24,7 @@ class FakeUserListPresenterFactory(
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
args: UserListPresenterArgs,
|
args: UserListPresenterArgs,
|
||||||
userListDataSource: UserListDataSource,
|
userRepository: UserRepository,
|
||||||
userListDataStore: UserListDataStore,
|
userListDataStore: UserListDataStore,
|
||||||
): UserListPresenter = fakeUserListPresenter
|
): UserListPresenter = fakeUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invitelist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
|
@ -103,7 +105,7 @@ fun InviteListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun InviteListContent(
|
fun InviteListContent(
|
||||||
state: InviteListState,
|
state: InviteListState,
|
||||||
|
|
@ -124,7 +126,9 @@ fun InviteListContent(
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(padding)
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
if (state.inviteList.isEmpty()) {
|
if (state.inviteList.isEmpty()) {
|
||||||
Spacer(Modifier.size(80.dp))
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -81,7 +83,7 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeServerView(
|
fun ChangeServerView(
|
||||||
state: ChangeServerState,
|
state: ChangeServerState,
|
||||||
|
|
@ -121,6 +123,7 @@ fun ChangeServerView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -86,7 +88,7 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginRootView(
|
fun LoginRootView(
|
||||||
state: LoginRootState,
|
state: LoginRootState,
|
||||||
|
|
@ -113,6 +115,7 @@ fun LoginRootView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,16 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(
|
|
||||||
ExperimentalMaterial3Api::class,
|
|
||||||
ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class,
|
|
||||||
)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
|
@ -90,6 +87,7 @@ import timber.log.Timber
|
||||||
|
|
||||||
import io.element.android.libraries.ui.strings.R as StringsR
|
import io.element.android.libraries.ui.strings.R as StringsR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesView(
|
fun MessagesView(
|
||||||
state: MessagesState,
|
state: MessagesState,
|
||||||
|
|
@ -198,7 +196,9 @@ fun MessagesView(
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
MessagesViewContent(
|
MessagesViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
onMessageClicked = ::onMessageClicked,
|
onMessageClicked = ::onMessageClicked,
|
||||||
onMessageLongClicked = ::onMessageLongClicked
|
onMessageLongClicked = ::onMessageLongClicked
|
||||||
)
|
)
|
||||||
|
|
@ -264,6 +264,7 @@ fun MessagesViewContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesViewTopBar(
|
fun MessagesViewTopBar(
|
||||||
roomTitle: String?,
|
roomTitle: String?,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterialApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.actionlist
|
package io.element.android.features.messages.impl.actionlist
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -49,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.ModalBottomShe
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ActionListView(
|
fun ActionListView(
|
||||||
state: ActionListState,
|
state: ActionListState,
|
||||||
|
|
@ -90,6 +89,7 @@ fun ActionListView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SheetContent(
|
private fun SheetContent(
|
||||||
state: ActionListState,
|
state: ActionListState,
|
||||||
|
|
@ -145,6 +145,7 @@ fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) s
|
||||||
fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: ActionListState) {
|
private fun ContentToPreview(state: ActionListState) {
|
||||||
ActionListView(
|
ActionListView(
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.textcomposer
|
package io.element.android.features.messages.textcomposer
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -46,13 +44,13 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
|
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -308,7 +306,7 @@ class MessageComposerPresenterTest {
|
||||||
mimetype = null,
|
mimetype = null,
|
||||||
size = null,
|
size = null,
|
||||||
thumbnailInfo = null,
|
thumbnailInfo = null,
|
||||||
thumbnailUrl = null,
|
thumbnailSource = null,
|
||||||
blurhash = null,
|
blurhash = null,
|
||||||
),
|
),
|
||||||
thumbnailInfo = ThumbnailProcessingInfo(
|
thumbnailInfo = ThumbnailProcessingInfo(
|
||||||
|
|
@ -351,7 +349,7 @@ class MessageComposerPresenterTest {
|
||||||
duration = null,
|
duration = null,
|
||||||
size = null,
|
size = null,
|
||||||
thumbnailInfo = null,
|
thumbnailInfo = null,
|
||||||
thumbnailUrl = null,
|
thumbnailSource = null,
|
||||||
blurhash = null,
|
blurhash = null,
|
||||||
),
|
),
|
||||||
thumbnailInfo = ThumbnailProcessingInfo(
|
thumbnailInfo = ThumbnailProcessingInfo(
|
||||||
|
|
@ -490,7 +488,12 @@ class MessageComposerPresenterTest {
|
||||||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||||
) = MessageComposerPresenter(
|
) = MessageComposerPresenter(
|
||||||
coroutineScope, room, pickerProvider, featureFlagService, mediaPreProcessor, snackbarDispatcher
|
coroutineScope,
|
||||||
|
room,
|
||||||
|
pickerProvider,
|
||||||
|
featureFlagService,
|
||||||
|
mediaPreProcessor,
|
||||||
|
snackbarDispatcher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
|
||||||
import com.google.accompanist.pager.HorizontalPager
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
import com.google.accompanist.pager.HorizontalPagerIndicator
|
import com.google.accompanist.pager.HorizontalPagerIndicator
|
||||||
import com.google.accompanist.pager.rememberPagerState
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
|
|
@ -53,9 +52,7 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnBoardingScreen(
|
fun OnBoardingScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - screenshot with success then dismiss`() = runTest {
|
fun `present - screenshot with success then dismiss`() = runTest(timeout = 30.seconds) {
|
||||||
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
||||||
val rageshake = FakeRageShake(isAvailableValue = true)
|
val rageshake = FakeRageShake(isAvailableValue = true)
|
||||||
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
||||||
|
|
@ -99,7 +99,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test(timeout = 30.seconds) {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.isStarted).isFalse()
|
assertThat(initialState.isStarted).isFalse()
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
api(projects.features.roomdetails.api)
|
api(projects.features.roomdetails.api)
|
||||||
|
api(projects.libraries.usersearch.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -50,7 +51,6 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ package io.element.android.features.roomdetails.impl
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -43,8 +45,8 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
||||||
import io.element.android.libraries.architecture.isLoading
|
import io.element.android.libraries.architecture.isLoading
|
||||||
|
|
@ -68,7 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomDetailsView(
|
fun RoomDetailsView(
|
||||||
state: RoomDetailsState,
|
state: RoomDetailsState,
|
||||||
|
|
@ -91,6 +93,7 @@ fun RoomDetailsView(
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
when (state.roomType) {
|
when (state.roomType) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.blockuser
|
package io.element.android.features.roomdetails.impl.blockuser
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Block
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -41,7 +42,7 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchResults by remember {
|
var searchResults by remember {
|
||||||
mutableStateOf<RoomMemberSearchResultState>(RoomMemberSearchResultState.NotSearching)
|
mutableStateOf<SearchBarResultState<RoomMembers>>(SearchBarResultState.NotSearching())
|
||||||
}
|
}
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -60,11 +61,11 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
searchResults = if (searchQuery.isEmpty()) {
|
searchResults = if (searchQuery.isEmpty()) {
|
||||||
RoomMemberSearchResultState.NotSearching
|
SearchBarResultState.NotSearching()
|
||||||
} else {
|
} else {
|
||||||
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
||||||
if (results.isEmpty()) RoomMemberSearchResultState.NoResults
|
if (results.isEmpty()) SearchBarResultState.NoResults()
|
||||||
else RoomMemberSearchResultState.Results(
|
else SearchBarResultState.Results(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomMemberListState(
|
data class RoomMemberListState(
|
||||||
val roomMembers: Async<RoomMembers>,
|
val roomMembers: Async<RoomMembers>,
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: RoomMemberSearchResultState,
|
val searchResults: SearchBarResultState<RoomMembers>,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
@ -32,14 +33,3 @@ data class RoomMembers(
|
||||||
val invited: ImmutableList<RoomMember>,
|
val invited: ImmutableList<RoomMember>,
|
||||||
val joined: ImmutableList<RoomMember>
|
val joined: ImmutableList<RoomMember>
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface RoomMemberSearchResultState {
|
|
||||||
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
|
||||||
object NotSearching : RoomMemberSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, but no results were found. */
|
|
||||||
object NoResults : RoomMemberSearchResultState
|
|
||||||
|
|
||||||
/** The search has completed, and some matching users were found. */
|
|
||||||
data class Results(val results: RoomMembers) : RoomMemberSearchResultState
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||||
override val values: Sequence<RoomMemberListState>
|
override val values: Sequence<RoomMemberListState>
|
||||||
|
|
@ -42,7 +42,7 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
aRoomMemberListState().copy(
|
aRoomMemberListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
searchResults = RoomMemberSearchResultState.Results(
|
searchResults = SearchBarResultState.Results(
|
||||||
RoomMembers(
|
RoomMembers(
|
||||||
invited = persistentListOf(aVictor()),
|
invited = persistentListOf(aVictor()),
|
||||||
joined = persistentListOf(anAlice()),
|
joined = persistentListOf(anAlice()),
|
||||||
|
|
@ -52,14 +52,14 @@ internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMember
|
||||||
aRoomMemberListState().copy(
|
aRoomMemberListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "something-with-no-results",
|
searchQuery = "something-with-no-results",
|
||||||
searchResults = RoomMemberSearchResultState.NoResults
|
searchResults = SearchBarResultState.NoResults()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomMemberListState(
|
internal fun aRoomMemberListState(
|
||||||
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||||
searchResults: RoomMemberSearchResultState = RoomMemberSearchResultState.NotSearching,
|
searchResults: SearchBarResultState<RoomMembers> = SearchBarResultState.NotSearching(),
|
||||||
) = RoomMemberListState(
|
) = RoomMemberListState(
|
||||||
roomMembers = roomMembers,
|
roomMembers = roomMembers,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,20 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -59,10 +52,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
@ -71,7 +63,7 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListView(
|
fun RoomMemberListView(
|
||||||
state: RoomMemberListState,
|
state: RoomMemberListState,
|
||||||
|
|
@ -94,27 +86,27 @@ fun RoomMemberListView(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
RoomMemberSearchBar(
|
||||||
RoomMemberSearchBar(
|
query = state.searchQuery,
|
||||||
query = state.searchQuery,
|
state = state.searchResults,
|
||||||
state = state.searchResults,
|
active = state.isSearchActive,
|
||||||
active = state.isSearchActive,
|
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
||||||
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
||||||
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
||||||
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
onUserSelected = ::onUserSelected,
|
||||||
onUserSelected = ::onUserSelected,
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
if (state.roomMembers is Async.Success) {
|
if (state.roomMembers is Async.Success) {
|
||||||
RoomMemberList(
|
RoomMemberList(
|
||||||
roomMembers = state.roomMembers.state,
|
roomMembers = state.roomMembers.state,
|
||||||
onUserSelected = ::onUserSelected,
|
showMembersCount = true,
|
||||||
|
onUserSelected = ::onUserSelected
|
||||||
)
|
)
|
||||||
} else if (state.roomMembers.isLoading()) {
|
} else if (state.roomMembers.isLoading()) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
|
@ -129,6 +121,7 @@ fun RoomMemberListView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberList(
|
private fun RoomMemberList(
|
||||||
roomMembers: RoomMembers,
|
roomMembers: RoomMembers,
|
||||||
|
showMembersCount: Boolean,
|
||||||
onUserSelected: (RoomMember) -> Unit,
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||||
|
|
@ -140,9 +133,15 @@ private fun RoomMemberList(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (roomMembers.joined.isNotEmpty()) {
|
if (roomMembers.joined.isNotEmpty()) {
|
||||||
val memberCount = roomMembers.joined.count()
|
|
||||||
roomMemberListSection(
|
roomMemberListSection(
|
||||||
headerText = { pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) },
|
headerText = {
|
||||||
|
if (showMembersCount) {
|
||||||
|
val memberCount = roomMembers.joined.count()
|
||||||
|
pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.screen_room_member_list_room_members_header_title)
|
||||||
|
}
|
||||||
|
},
|
||||||
members = roomMembers.joined,
|
members = roomMembers.joined,
|
||||||
onMemberSelected = { onUserSelected(it) }
|
onMemberSelected = { onUserSelected(it) }
|
||||||
)
|
)
|
||||||
|
|
@ -209,11 +208,10 @@ private fun RoomMemberListTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberSearchBar(
|
private fun RoomMemberSearchBar(
|
||||||
query: String,
|
query: String,
|
||||||
state: RoomMemberSearchResultState,
|
state: SearchBarResultState<RoomMembers>,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
placeHolderTitle: String,
|
placeHolderTitle: String,
|
||||||
onActiveChanged: (Boolean) -> Unit,
|
onActiveChanged: (Boolean) -> Unit,
|
||||||
|
|
@ -221,71 +219,21 @@ private fun RoomMemberSearchBar(
|
||||||
onUserSelected: (RoomMember) -> Unit,
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
if (!active) {
|
|
||||||
onTextChanged("")
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = query,
|
query = query,
|
||||||
onQueryChange = onTextChanged,
|
onQueryChange = onTextChanged,
|
||||||
onSearch = { focusManager.clearFocus() },
|
|
||||||
active = active,
|
active = active,
|
||||||
onActiveChange = onActiveChanged,
|
onActiveChange = onActiveChanged,
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
placeHolderTitle = placeHolderTitle,
|
||||||
placeholder = {
|
resultState = state,
|
||||||
Text(
|
resultHandler = { results ->
|
||||||
text = placeHolderTitle,
|
RoomMemberList(
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
roomMembers = results,
|
||||||
|
showMembersCount = false,
|
||||||
|
onUserSelected = { onUserSelected(it) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
leadingIcon = if (active) {
|
|
||||||
{ BackButton(onClick = { onActiveChanged(false) }) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
trailingIcon = when {
|
|
||||||
active && query.isNotEmpty() -> {
|
|
||||||
{
|
|
||||||
IconButton(onClick = { onTextChanged("") }) {
|
|
||||||
Icon(Icons.Default.Close, stringResource(StringR.string.action_clear))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!active -> {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = stringResource(StringR.string.action_search),
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
|
||||||
content = {
|
|
||||||
if (state is RoomMemberSearchResultState.Results) {
|
|
||||||
RoomMemberList(
|
|
||||||
roomMembers = state.results,
|
|
||||||
onUserSelected = { onUserSelected(it) }
|
|
||||||
)
|
|
||||||
} else if (state is RoomMemberSearchResultState.NoResults) {
|
|
||||||
Spacer(Modifier.size(80.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(StringR.string.common_no_results),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ package io.element.android.features.roomdetails.impl.members.details
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -39,8 +41,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserDialogs
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.roomdetails.blockuser.BlockUserSection
|
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
@ -57,7 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberDetailsView(
|
fun RoomMemberDetailsView(
|
||||||
state: RoomMemberDetailsState,
|
state: RoomMemberDetailsState,
|
||||||
|
|
@ -74,6 +76,7 @@ fun RoomMemberDetailsView(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
RoomMemberHeaderSection(
|
RoomMemberHeaderSection(
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@
|
||||||
<item quantity="one">"1 person"</item>
|
<item quantity="one">"1 person"</item>
|
||||||
<item quantity="other">"%1$d people"</item>
|
<item quantity="other">"%1$d people"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="screen_room_details_already_a_member">"Already a member"</string>
|
||||||
|
<string name="screen_room_details_already_invited">"Already invited"</string>
|
||||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||||
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
|
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
|
||||||
|
<string name="screen_room_member_list_room_members_header_title">"Room members"</string>
|
||||||
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
||||||
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||||
<string name="screen_dm_details_block_user">"Block user"</string>
|
<string name="screen_dm_details_block_user">"Block user"</string>
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,12 @@ import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberSearchResultState
|
|
||||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||||
import io.element.android.features.roomdetails.impl.members.aWalter
|
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -49,7 +49,7 @@ class RoomMemberListPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
||||||
Truth.assertThat(initialState.searchQuery).isEmpty()
|
Truth.assertThat(initialState.searchQuery).isEmpty()
|
||||||
Truth.assertThat(initialState.searchResults).isEqualTo(RoomMemberSearchResultState.NotSearching)
|
Truth.assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
Truth.assertThat(initialState.isSearchActive).isFalse()
|
Truth.assertThat(initialState.isSearchActive).isFalse()
|
||||||
|
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
|
|
@ -89,7 +89,7 @@ class RoomMemberListPresenterTests {
|
||||||
val searchQueryUpdatedState = awaitItem()
|
val searchQueryUpdatedState = awaitItem()
|
||||||
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
||||||
val searchSearchResultDelivered = awaitItem()
|
val searchSearchResultDelivered = awaitItem()
|
||||||
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.NoResults::class.java)
|
Truth.assertThat(searchSearchResultDelivered.searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,8 +107,8 @@ class RoomMemberListPresenterTests {
|
||||||
val searchQueryUpdatedState = awaitItem()
|
val searchQueryUpdatedState = awaitItem()
|
||||||
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
||||||
val searchSearchResultDelivered = awaitItem()
|
val searchSearchResultDelivered = awaitItem()
|
||||||
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.Results::class.java)
|
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
Truth.assertThat((searchSearchResultDelivered.searchResults as RoomMemberSearchResultState.Results).results.joined.first().displayName)
|
Truth.assertThat((searchSearchResultDelivered.searchResults as SearchBarResultState.Results).results.joined.first().displayName)
|
||||||
.isEqualTo("Alice")
|
.isEqualTo("Alice")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -116,7 +118,7 @@ fun RoomListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListContent(
|
fun RoomListContent(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
|
|
@ -190,6 +192,7 @@ fun RoomListContent(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.features.roomlist.impl.components
|
package io.element.android.features.roomlist.impl.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -30,7 +31,6 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.copy
|
import io.element.android.libraries.designsystem.utils.copy
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -90,7 +91,7 @@ internal fun RoomListSearchResultView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomListSearchResultContent(
|
internal fun RoomListSearchResultContent(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
|
|
@ -128,12 +129,14 @@ internal fun RoomListSearchResultContent(
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
value = filter,
|
value = filter,
|
||||||
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
onValueChange = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
errorIndicatorColor = Color.Transparent,
|
errorIndicatorColor = Color.Transparent,
|
||||||
disabledIndicatorColor = Color.Transparent
|
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (filter.isNotEmpty()) {
|
if (filter.isNotEmpty()) {
|
||||||
|
|
@ -182,6 +185,7 @@ internal fun RoomListSearchResultContent(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import io.element.android.libraries.ui.strings.R
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun SearchUserBar(
|
|
||||||
query: String,
|
|
||||||
state: UserSearchResultState,
|
|
||||||
selectedUsers: ImmutableList<MatrixUser>,
|
|
||||||
active: Boolean,
|
|
||||||
isMultiSelectionEnabled: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
|
||||||
onActiveChanged: (Boolean) -> Unit = {},
|
|
||||||
onTextChanged: (String) -> Unit = {},
|
|
||||||
onUserSelected: (MatrixUser) -> Unit = {},
|
|
||||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
if (!active) {
|
|
||||||
onTextChanged("")
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchBar(
|
|
||||||
query = query,
|
|
||||||
onQueryChange = onTextChanged,
|
|
||||||
onSearch = { focusManager.clearFocus() },
|
|
||||||
active = active,
|
|
||||||
onActiveChange = onActiveChanged,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = placeHolderTitle,
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = if (active) {
|
|
||||||
{ BackButton(onClick = { onActiveChanged(false) }) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
trailingIcon = when {
|
|
||||||
active && query.isNotEmpty() -> {
|
|
||||||
{
|
|
||||||
IconButton(onClick = { onTextChanged("") }) {
|
|
||||||
Icon(Icons.Default.Close, stringResource(R.string.action_clear))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!active -> {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = stringResource(R.string.action_search),
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
|
||||||
content = {
|
|
||||||
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
|
||||||
SelectedUsersList(
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
selectedUsers = selectedUsers,
|
|
||||||
autoScroll = true,
|
|
||||||
onUserRemoved = onUserDeselected,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (state is UserSearchResultState.Results) {
|
|
||||||
LazyColumn {
|
|
||||||
if (isMultiSelectionEnabled) {
|
|
||||||
items(state.results) { matrixUser ->
|
|
||||||
SearchMultipleUsersResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
if (checked) {
|
|
||||||
onUserSelected(matrixUser)
|
|
||||||
} else {
|
|
||||||
onUserDeselected(matrixUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items(state.results) { matrixUser ->
|
|
||||||
SearchSingleUserResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
onClick = { onUserSelected(matrixUser) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (state is UserSearchResultState.NoResults) {
|
|
||||||
Spacer(Modifier.size(80.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.common_no_results),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ startup = "1.1.1"
|
||||||
media3 = "1.0.1"
|
media3 = "1.0.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2023.04.01"
|
compose_bom = "2023.05.01"
|
||||||
composecompiler = "1.4.7"
|
composecompiler = "1.4.7"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
|
|
@ -33,7 +33,7 @@ accompanist = "0.30.1"
|
||||||
test_core = "1.5.0"
|
test_core = "1.5.0"
|
||||||
|
|
||||||
#other
|
#other
|
||||||
coil = "2.3.0"
|
coil = "2.4.0"
|
||||||
datetime = "0.4.0"
|
datetime = "0.4.0"
|
||||||
serialization_json = "1.5.0"
|
serialization_json = "1.5.0"
|
||||||
showkase = "1.0.0-beta18"
|
showkase = "1.0.0-beta18"
|
||||||
|
|
@ -119,7 +119,7 @@ test_orchestrator = "androidx.test:orchestrator:1.4.2"
|
||||||
test_turbine = "app.cash.turbine:turbine:0.12.3"
|
test_turbine = "app.cash.turbine:turbine:0.12.3"
|
||||||
test_truth = "com.google.truth:truth:1.1.3"
|
test_truth = "com.google.truth:truth:1.1.3"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11"
|
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11"
|
||||||
test_robolectric = "org.robolectric:robolectric:4.10.2"
|
test_robolectric = "org.robolectric:robolectric:4.10.3"
|
||||||
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
|
|
@ -133,7 +133,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.13"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.14"
|
||||||
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ package io.element.android.libraries.designsystem.components.preferences
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -45,7 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PreferenceView(
|
fun PreferenceView(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -70,6 +72,7 @@ fun PreferenceView(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(it)
|
.padding(it)
|
||||||
|
.consumeWindowInsets(it)
|
||||||
.verticalScroll(
|
.verticalScroll(
|
||||||
state = scrollState,
|
state = scrollState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
|
@ -57,6 +55,7 @@ fun MediumTopAppBar(
|
||||||
internal fun MediumTopAppBarPreview() =
|
internal fun MediumTopAppBarPreview() =
|
||||||
ElementThemedPreview { ContentToPreview() }
|
ElementThemedPreview { ContentToPreview() }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
MediumTopAppBar(title = { Text(text = "Title") })
|
MediumTopAppBar(title = { Text(text = "Title") })
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -107,6 +105,7 @@ internal fun ModalBottomSheetLayoutLightPreview() =
|
||||||
internal fun ModalBottomSheetLayoutDarkPreview() =
|
internal fun ModalBottomSheetLayoutDarkPreview() =
|
||||||
ElementPreviewDark { ContentToPreview() }
|
ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
ModalBottomSheetLayout(
|
ModalBottomSheetLayout(
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,9 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.TextFieldColors
|
import androidx.compose.material3.TextFieldColors
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
|
@ -49,7 +48,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
import io.element.android.libraries.designsystem.utils.allBooleans
|
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||||
import io.element.android.libraries.designsystem.utils.asInt
|
import io.element.android.libraries.designsystem.utils.asInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OutlinedTextField(
|
fun OutlinedTextField(
|
||||||
value: String,
|
value: String,
|
||||||
|
|
@ -70,8 +68,8 @@ fun OutlinedTextField(
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
shape: Shape = TextFieldDefaults.outlinedShape,
|
shape: Shape = OutlinedTextFieldDefaults.shape,
|
||||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
|
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.OutlinedTextField(
|
androidx.compose.material3.OutlinedTextField(
|
||||||
value = value,
|
value = value,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ScaffoldDefaults
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
|
|
@ -27,7 +26,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Scaffold(
|
fun Scaffold(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -14,77 +14,218 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SearchBarColors
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun <T> SearchBar(
|
||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
onActiveChange: (Boolean) -> Unit,
|
onActiveChange: (Boolean) -> Unit,
|
||||||
|
placeHolderTitle: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
placeholder: @Composable (() -> Unit)? = null,
|
resultState: SearchBarResultState<T> = SearchBarResultState.NotSearching(),
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
|
||||||
tonalElevation: Dp = SearchBarDefaults.Elevation,
|
tonalElevation: Dp = SearchBarDefaults.Elevation,
|
||||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
resultHandler: @Composable ColumnScope.(T) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
onQueryChange("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
androidx.compose.material3.SearchBar(
|
androidx.compose.material3.SearchBar(
|
||||||
query = query,
|
query = query,
|
||||||
onQueryChange = onQueryChange,
|
onQueryChange = onQueryChange,
|
||||||
onSearch = onSearch,
|
onSearch = { focusManager.clearFocus() },
|
||||||
active = active,
|
active = active,
|
||||||
onActiveChange = onActiveChange,
|
onActiveChange = onActiveChange,
|
||||||
modifier = modifier,
|
modifier = modifier.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
placeholder = placeholder,
|
placeholder = {
|
||||||
leadingIcon = leadingIcon,
|
Text(
|
||||||
trailingIcon = trailingIcon,
|
text = placeHolderTitle,
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = if (active) {
|
||||||
|
{ BackButton(onClick = { onActiveChange(false) }) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
trailingIcon = when {
|
||||||
|
active && query.isNotEmpty() -> {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onQueryChange("") }) {
|
||||||
|
Icon(Icons.Default.Close, stringResource(R.string.action_clear))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!active -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = stringResource(R.string.action_search),
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
shape = shape,
|
shape = shape,
|
||||||
colors = colors,
|
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
windowInsets = windowInsets,
|
windowInsets = windowInsets,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
content = content,
|
content = {
|
||||||
|
contentPrefix()
|
||||||
|
when (resultState) {
|
||||||
|
is SearchBarResultState.Results<T> -> {
|
||||||
|
resultHandler(resultState.results)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchBarResultState.NoResults<T> -> {
|
||||||
|
// No results found, show a message
|
||||||
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.common_no_results),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Not searching - nothing to show.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentSuffix()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SearchBarResultState<in T> {
|
||||||
|
/** No search results are available yet (e.g. because the user hasn't entered a search term). */
|
||||||
|
class NotSearching<T> : SearchBarResultState<T>
|
||||||
|
|
||||||
|
/** The search has completed, but no results were found. */
|
||||||
|
class NoResults<T> : SearchBarResultState<T>
|
||||||
|
|
||||||
|
/** The search has completed, and some matching users were found. */
|
||||||
|
data class Results<T>(val results: T) : SearchBarResultState<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Search)
|
||||||
|
@Composable
|
||||||
|
internal fun SearchBarPreviewInactive() = ElementThemedPreview { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Search)
|
||||||
|
@Composable
|
||||||
|
internal fun SearchBarPreviewActiveEmptyQuery() = ElementThemedPreview {
|
||||||
|
ContentToPreview(
|
||||||
|
query = "",
|
||||||
|
active = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Search)
|
@Preview(group = PreviewGroup.Search)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DockedSearchBarPreview() = ElementThemedPreview { ContentToPreview() }
|
internal fun SearchBarPreviewActiveWithQuery() = ElementThemedPreview {
|
||||||
|
ContentToPreview(
|
||||||
@Composable
|
query = "search term",
|
||||||
private fun ContentToPreview() {
|
active = true,
|
||||||
SearchBar(
|
)
|
||||||
query = "Some text",
|
}
|
||||||
onQueryChange = {},
|
|
||||||
onSearch = {},
|
@Preview(group = PreviewGroup.Search)
|
||||||
active = false,
|
@Composable
|
||||||
onActiveChange = {},
|
internal fun SearchBarPreviewActiveWithNoResults() = ElementThemedPreview {
|
||||||
content = {},
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.TextFieldColors
|
import androidx.compose.material3.TextFieldColors
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
|
@ -50,7 +49,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
import io.element.android.libraries.designsystem.utils.allBooleans
|
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||||
import io.element.android.libraries.designsystem.utils.asInt
|
import io.element.android.libraries.designsystem.utils.asInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextField(
|
fun TextField(
|
||||||
value: String,
|
value: String,
|
||||||
|
|
@ -71,8 +69,8 @@ fun TextField(
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
shape: Shape = TextFieldDefaults.filledShape,
|
shape: Shape = TextFieldDefaults.shape,
|
||||||
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
|
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.TextField(
|
androidx.compose.material3.TextField(
|
||||||
value = value,
|
value = value,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.theme.components
|
package io.element.android.libraries.designsystem.theme.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ interface MatrixClient : Closeable {
|
||||||
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
|
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
|
||||||
suspend fun createDM(userId: UserId): Result<RoomId>
|
suspend fun createDM(userId: UserId): Result<RoomId>
|
||||||
suspend fun getProfile(userId: UserId): Result<MatrixUser>
|
suspend fun getProfile(userId: UserId): Result<MatrixUser>
|
||||||
|
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
|
||||||
fun startSync()
|
fun startSync()
|
||||||
fun stopSync()
|
fun stopSync()
|
||||||
fun sessionVerificationService(): SessionVerificationService
|
fun sessionVerificationService(): SessionVerificationService
|
||||||
|
|
@ -51,10 +52,7 @@ interface MatrixClient : Closeable {
|
||||||
suspend fun logout()
|
suspend fun logout()
|
||||||
suspend fun loadUserDisplayName(): Result<String>
|
suspend fun loadUserDisplayName(): Result<String>
|
||||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||||
|
suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String>
|
||||||
fun onSlidingSyncUpdate()
|
fun onSlidingSyncUpdate()
|
||||||
|
|
||||||
fun roomMembershipObserver(): RoomMembershipObserver
|
fun roomMembershipObserver(): RoomMembershipObserver
|
||||||
|
|
||||||
suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults>
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl
|
package io.element.android.libraries.matrix.impl
|
||||||
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
|
@ -46,7 +44,6 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
|
@ -284,6 +281,13 @@ class RustMatrixClient constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun sessionVerificationService(): SessionVerificationService = verificationService
|
override fun sessionVerificationService(): SessionVerificationService = verificationService
|
||||||
|
|
||||||
override fun pushersService(): PushersService = pushersService
|
override fun pushersService(): PushersService = pushersService
|
||||||
|
|
@ -341,6 +345,13 @@ class RustMatrixClient constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> = withContext(dispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
client.uploadMedia(mimeType, data.toUByteArray().toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSlidingSyncUpdate() {
|
override fun onSlidingSyncUpdate() {
|
||||||
if (!verificationService.isReady.value) {
|
if (!verificationService.isReady.value) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -353,13 +364,6 @@ class RustMatrixClient constructor(
|
||||||
|
|
||||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||||
|
|
||||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
|
|
||||||
withContext(dispatchers.io) {
|
|
||||||
runCatching {
|
|
||||||
client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun File.deleteSessionDirectory(userID: String): Boolean {
|
private fun File.deleteSessionDirectory(userID: String): Boolean {
|
||||||
// Rust sanitises the user ID replacing invalid characters with an _
|
// Rust sanitises the user ID replacing invalid characters with an _
|
||||||
val sanitisedUserID = userID.replace(":", "_")
|
val sanitisedUserID = userID.replace(":", "_")
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ internal class RustRoomSummaryDataSource(
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
//@OptIn(FlowPreview::class)
|
|
||||||
override fun roomSummaries(): StateFlow<List<RoomSummary>> {
|
override fun roomSummaries(): StateFlow<List<RoomSummary>> {
|
||||||
return roomSummaries
|
return roomSummaries
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ class FakeMatrixClient(
|
||||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||||
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||||
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
||||||
|
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
|
||||||
|
|
||||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||||
return getRoomResults[roomId]
|
return getRoomResults[roomId]
|
||||||
|
|
@ -92,6 +93,10 @@ class FakeMatrixClient(
|
||||||
return getProfileResults[userId] ?: Result.failure(IllegalStateException("No profile found for $userId"))
|
return getProfileResults[userId] ?: Result.failure(IllegalStateException("No profile found for $userId"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> {
|
||||||
|
return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm"))
|
||||||
|
}
|
||||||
|
|
||||||
override fun startSync() = Unit
|
override fun startSync() = Unit
|
||||||
|
|
||||||
override fun stopSync() = Unit
|
override fun stopSync() = Unit
|
||||||
|
|
@ -111,6 +116,10 @@ class FakeMatrixClient(
|
||||||
return userAvatarURLString
|
return userAvatarURLString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result<String> {
|
||||||
|
return uploadMediaResult
|
||||||
|
}
|
||||||
|
|
||||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||||
|
|
||||||
override fun pushersService(): PushersService = pushersService
|
override fun pushersService(): PushersService = pushersService
|
||||||
|
|
@ -123,10 +132,6 @@ class FakeMatrixClient(
|
||||||
return RoomMembershipObserver()
|
return RoomMembershipObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> {
|
|
||||||
return searchUserResults[searchTerm] ?: Result.failure(IllegalStateException("No response defined for $searchTerm"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
|
|
||||||
fun givenLogoutError(failure: Throwable?) {
|
fun givenLogoutError(failure: Throwable?) {
|
||||||
|
|
@ -168,4 +173,8 @@ class FakeMatrixClient(
|
||||||
fun givenGetProfileResult(userId: UserId, result: Result<MatrixUser>) {
|
fun givenGetProfileResult(userId: UserId, result: Result<MatrixUser>) {
|
||||||
getProfileResults[userId] = result
|
getProfileResults[userId] = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenUploadMediaResult(result: Result<String>) {
|
||||||
|
uploadMediaResult = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,14 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CheckableMatrixUserRow(
|
fun CheckableMatrixUserRow(
|
||||||
|
|
@ -40,18 +43,39 @@ fun CheckableMatrixUserRow(
|
||||||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||||
onCheckedChange: (Boolean) -> Unit = {},
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) = CheckableUserRow(
|
||||||
|
checked = checked,
|
||||||
|
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||||
|
name = matrixUser.getBestName(),
|
||||||
|
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||||
|
modifier = modifier,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
enabled = enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CheckableUserRow(
|
||||||
|
checked: Boolean,
|
||||||
|
avatarData: AvatarData,
|
||||||
|
name: String,
|
||||||
|
subtext: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(role = Role.Checkbox) { onCheckedChange(!checked) },
|
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||||
|
onCheckedChange(!checked)
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
MatrixUserRow(
|
UserRow(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
matrixUser = matrixUser,
|
avatarData = avatarData,
|
||||||
avatarSize = avatarSize,
|
name = name,
|
||||||
|
subtext = subtext,
|
||||||
)
|
)
|
||||||
|
|
||||||
Checkbox(
|
Checkbox(
|
||||||
|
|
@ -77,5 +101,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
Column {
|
Column {
|
||||||
CheckableMatrixUserRow(checked = true, matrixUser)
|
CheckableMatrixUserRow(checked = true, matrixUser)
|
||||||
CheckableMatrixUserRow(checked = false, matrixUser)
|
CheckableMatrixUserRow(checked = false, matrixUser)
|
||||||
|
CheckableMatrixUserRow(checked = true, matrixUser, enabled = false)
|
||||||
|
CheckableMatrixUserRow(checked = false, matrixUser, enabled = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -46,6 +47,19 @@ fun MatrixUserRow(
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||||
|
) = UserRow(
|
||||||
|
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||||
|
name = matrixUser.getBestName(),
|
||||||
|
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserRow(
|
||||||
|
avatarData: AvatarData,
|
||||||
|
name: String,
|
||||||
|
subtext: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -54,9 +68,7 @@ fun MatrixUserRow(
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(avatarData)
|
||||||
matrixUser.getAvatarData(size = avatarSize),
|
|
||||||
)
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp),
|
.padding(start = 12.dp),
|
||||||
|
|
@ -65,15 +77,15 @@ fun MatrixUserRow(
|
||||||
Text(
|
Text(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
text = matrixUser.getBestName(),
|
text = name,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
// Id
|
// Id
|
||||||
if (matrixUser.displayName.isNullOrEmpty().not()) {
|
subtext?.let {
|
||||||
Text(
|
Text(
|
||||||
text = matrixUser.userId.value,
|
text = subtext,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -41,10 +41,9 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedUser(
|
fun SelectedUser(
|
||||||
|
|
@ -74,7 +73,7 @@ fun SelectedUser(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = stringResource(id = R.string.action_remove),
|
contentDescription = stringResource(id = StringR.string.action_remove),
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api.components
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -30,11 +30,11 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedUsersList(
|
fun SelectedUsersList(
|
||||||
|
|
@ -82,6 +82,6 @@ internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPrev
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
SelectedUsersList(
|
SelectedUsersList(
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +27,11 @@ interface PickerProvider {
|
||||||
onResult: (uri: Uri?, mimeType: String?) -> Unit
|
onResult: (uri: Uri?, mimeType: String?) -> Unit
|
||||||
): PickerLauncher<PickVisualMediaRequest, Uri?>
|
): PickerLauncher<PickVisualMediaRequest, Uri?>
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun registerGalleryImagePicker(
|
||||||
|
onResult: (Uri?) -> Unit
|
||||||
|
): PickerLauncher<PickVisualMediaRequest, Uri?>
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun registerFilePicker(
|
fun registerFilePicker(
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
|
|
@ -38,5 +43,4 @@ interface PickerProvider {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean>
|
fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,13 @@ sealed interface PickerType<Input, Output> {
|
||||||
fun getContract(): ActivityResultContract<Input, Output>
|
fun getContract(): ActivityResultContract<Input, Output>
|
||||||
fun getDefaultRequest(): Input
|
fun getDefaultRequest(): Input
|
||||||
|
|
||||||
|
object Image : PickerType<PickVisualMediaRequest, Uri?> {
|
||||||
|
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||||
|
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||||
|
return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object ImageAndVideo : PickerType<PickVisualMediaRequest, Uri?> {
|
object ImageAndVideo : PickerType<PickVisualMediaRequest, Uri?> {
|
||||||
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
override fun getContract() = ActivityResultContracts.PickVisualMedia()
|
||||||
override fun getDefaultRequest(): PickVisualMediaRequest {
|
override fun getDefaultRequest(): PickVisualMediaRequest {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,20 @@ class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers and returns a [PickerLauncher] for a gallery picture.
|
||||||
|
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
override fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
|
||||||
|
// Tests and UI preview can't handle Contexts, so we might as well disable the whole picker
|
||||||
|
return if (LocalInspectionMode.current || isInTest) {
|
||||||
|
NoOpPickerLauncher { onResult(null) }
|
||||||
|
} else {
|
||||||
|
rememberPickerLauncher(type = PickerType.Image) { uri -> onResult(uri) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
|
* Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video.
|
||||||
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ class FakePickerProvider : PickerProvider {
|
||||||
return NoOpPickerLauncher { onResult(result, mimeType) }
|
return NoOpPickerLauncher { onResult(result, mimeType) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun registerGalleryImagePicker(onResult: (uri: Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
|
||||||
|
return NoOpPickerLauncher { onResult(result) }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun registerFilePicker(mimeType: String, onResult: (Uri?) -> Unit): PickerLauncher<String, Uri?> {
|
override fun registerFilePicker(mimeType: String, onResult: (Uri?) -> Unit): PickerLauncher<String, Uri?> {
|
||||||
return NoOpPickerLauncher { onResult(result) }
|
return NoOpPickerLauncher { onResult(result) }
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,13 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
sealed interface MediaUploadInfo {
|
sealed interface MediaUploadInfo {
|
||||||
data class Image(val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
|
||||||
data class Video(val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
val file: File
|
||||||
data class Audio(val file: File, val info: AudioInfo) : MediaUploadInfo
|
|
||||||
data class AnyFile(val file: File, val info: FileInfo) : MediaUploadInfo
|
data class Image(override val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||||
|
data class Video(override val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo
|
||||||
|
data class Audio(override val file: File, val info: AudioInfo) : MediaUploadInfo
|
||||||
|
data class AnyFile(override val file: File, val info: FileInfo) : MediaUploadInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ThumbnailProcessingInfo(
|
data class ThumbnailProcessingInfo(
|
||||||
|
|
|
||||||
|
|
@ -130,19 +130,15 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
}.mapFailure { MediaPreProcessor.Failure(it) }
|
}.mapFailure { MediaPreProcessor.Failure(it) }
|
||||||
|
|
||||||
private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo {
|
private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo {
|
||||||
|
|
||||||
fun File.rename(name: String): File {
|
|
||||||
return File(context.cacheDir, name).also {
|
|
||||||
renameTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val name = context.getFileName(uri) ?: return this
|
val name = context.getFileName(uri) ?: return this
|
||||||
|
val renamedFile = File(context.cacheDir, name).also {
|
||||||
|
file.renameTo(it)
|
||||||
|
}
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is MediaUploadInfo.AnyFile -> copy(file = file.rename(name))
|
is MediaUploadInfo.AnyFile -> copy(file = renamedFile)
|
||||||
is MediaUploadInfo.Audio -> copy(file = file.rename(name))
|
is MediaUploadInfo.Audio -> copy(file = renamedFile)
|
||||||
is MediaUploadInfo.Image -> copy(file = file.rename(name))
|
is MediaUploadInfo.Image -> copy(file = renamedFile)
|
||||||
is MediaUploadInfo.Video -> copy(file = file.rename(name))
|
is MediaUploadInfo.Video -> copy(file = renamedFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
<string name="common_developer_options">"Developer options"</string>
|
<string name="common_developer_options">"Developer options"</string>
|
||||||
<string name="common_edited_suffix">"(edited)"</string>
|
<string name="common_edited_suffix">"(edited)"</string>
|
||||||
<string name="common_editing">"Editing"</string>
|
<string name="common_editing">"Editing"</string>
|
||||||
|
<string name="common_emote">"* %1$s %2$s"</string>
|
||||||
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
||||||
<string name="common_error">"Error"</string>
|
<string name="common_error">"Error"</string>
|
||||||
<string name="common_file">"File"</string>
|
<string name="common_file">"File"</string>
|
||||||
|
|
@ -84,12 +85,14 @@
|
||||||
<string name="common_report_a_bug">"Report a bug"</string>
|
<string name="common_report_a_bug">"Report a bug"</string>
|
||||||
<string name="common_report_submitted">"Report submitted"</string>
|
<string name="common_report_submitted">"Report submitted"</string>
|
||||||
<string name="common_search_for_someone">"Search for someone"</string>
|
<string name="common_search_for_someone">"Search for someone"</string>
|
||||||
|
<string name="common_search_results">"Search results"</string>
|
||||||
<string name="common_security">"Security"</string>
|
<string name="common_security">"Security"</string>
|
||||||
<string name="common_select_your_server">"Select your server"</string>
|
<string name="common_select_your_server">"Select your server"</string>
|
||||||
<string name="common_sending">"Sending…"</string>
|
<string name="common_sending">"Sending…"</string>
|
||||||
<string name="common_server_not_supported">"Server not supported"</string>
|
<string name="common_server_not_supported">"Server not supported"</string>
|
||||||
<string name="common_server_url">"Server URL"</string>
|
<string name="common_server_url">"Server URL"</string>
|
||||||
<string name="common_settings">"Settings"</string>
|
<string name="common_settings">"Settings"</string>
|
||||||
|
<string name="common_starting_chat">"Starting chat…"</string>
|
||||||
<string name="common_sticker">"Sticker"</string>
|
<string name="common_sticker">"Sticker"</string>
|
||||||
<string name="common_success">"Success"</string>
|
<string name="common_success">"Success"</string>
|
||||||
<string name="common_suggestions">"Suggestions"</string>
|
<string name="common_suggestions">"Suggestions"</string>
|
||||||
|
|
@ -159,4 +162,4 @@
|
||||||
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
||||||
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
||||||
<string name="screen_report_content_block_user">"Block user"</string>
|
<string name="screen_report_content_block_user">"Block user"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,16 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-library")
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.userlist.api"
|
namespace = "io.element.android.libraries.usersearch.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
|
||||||
ksp(libs.showkase.processor)
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.libraries.usersearch.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -14,22 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.di
|
package io.element.android.libraries.usersearch.api
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import dagger.Binds
|
import kotlinx.coroutines.flow.Flow
|
||||||
import dagger.Module
|
|
||||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
@Module
|
interface UserRepository {
|
||||||
@ContributesTo(SessionScope::class)
|
|
||||||
interface CreateRoomModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Named("AllUsers")
|
|
||||||
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): UserListDataSource
|
|
||||||
|
|
||||||
|
suspend fun search(query: String): Flow<List<MatrixUser>>
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -15,13 +15,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-library")
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.userlist.impl"
|
namespace = "io.element.android.libraries.usersearch.impl"
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -29,28 +28,18 @@ anvil {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.anvilannotations)
|
|
||||||
anvil(projects.anvilcodegen)
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.elementresources)
|
api(projects.libraries.usersearch.api)
|
||||||
implementation(projects.libraries.testtags)
|
|
||||||
implementation(projects.libraries.uiStrings)
|
|
||||||
api(projects.features.userlist.api)
|
|
||||||
ksp(libs.showkase.processor)
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.coroutines.core)
|
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,15 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.libraries.usersearch.impl
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AllMatrixUsersDataSource @Inject constructor(
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class MatrixUserListDataSource @Inject constructor(
|
||||||
private val client: MatrixClient
|
private val client: MatrixClient
|
||||||
) : UserListDataSource {
|
) : UserListDataSource {
|
||||||
override suspend fun search(query: String): List<MatrixUser> {
|
override suspend fun search(query: String): List<MatrixUser> {
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.usersearch.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class MatrixUserRepository @Inject constructor(
|
||||||
|
private val dataSource: UserListDataSource
|
||||||
|
) : UserRepository {
|
||||||
|
|
||||||
|
override suspend fun search(query: String): Flow<List<MatrixUser>> = flow {
|
||||||
|
// Manually add a fake result with the matrixId, if any
|
||||||
|
val isUserId = MatrixPatterns.isUserId(query)
|
||||||
|
if (isUserId) {
|
||||||
|
emit(listOf(MatrixUser(UserId(query))))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length >= MINIMUM_SEARCH_LENGTH) {
|
||||||
|
// Debounce
|
||||||
|
delay(DEBOUNCE_TIME_MILLIS)
|
||||||
|
|
||||||
|
val results = dataSource.search(query).toMutableList()
|
||||||
|
|
||||||
|
// If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly
|
||||||
|
if (isUserId && results.none { it.userId.value == query }) {
|
||||||
|
val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query))
|
||||||
|
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||||
|
results.add(0, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEBOUNCE_TIME_MILLIS = 500L
|
||||||
|
private const val MINIMUM_SEARCH_LENGTH = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.libraries.usersearch.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
internal class AllMatrixUsersDataSourceTest {
|
internal class MatrixUserListDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `search - returns users on success`() = runTest {
|
fun `search - returns users on success`() = runTest {
|
||||||
|
|
@ -37,15 +37,21 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
searchTerm = "test",
|
searchTerm = "test",
|
||||||
result = Result.success(
|
result = Result.success(
|
||||||
MatrixSearchUserResults(
|
MatrixSearchUserResults(
|
||||||
results = listOf(aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2)),
|
results = listOf(
|
||||||
|
aMatrixUserProfile(),
|
||||||
|
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||||
|
),
|
||||||
limited = false
|
limited = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val results = dataSource.search("test")
|
val results = dataSource.search("test")
|
||||||
Truth.assertThat(results).containsExactly(aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2))
|
Truth.assertThat(results).containsExactly(
|
||||||
|
aMatrixUserProfile(),
|
||||||
|
aMatrixUserProfile(userId = A_USER_ID_2)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -55,7 +61,7 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
searchTerm = "test",
|
searchTerm = "test",
|
||||||
result = Result.failure(Throwable("Ruhroh"))
|
result = Result.failure(Throwable("Ruhroh"))
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val results = dataSource.search("test")
|
val results = dataSource.search("test")
|
||||||
Truth.assertThat(results).isEmpty()
|
Truth.assertThat(results).isEmpty()
|
||||||
|
|
@ -68,7 +74,7 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
result = Result.success(aMatrixUserProfile())
|
result = Result.success(aMatrixUserProfile())
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val result = dataSource.getProfile(A_USER_ID)
|
val result = dataSource.getProfile(A_USER_ID)
|
||||||
Truth.assertThat(result).isEqualTo(aMatrixUserProfile())
|
Truth.assertThat(result).isEqualTo(aMatrixUserProfile())
|
||||||
|
|
@ -81,7 +87,7 @@ internal class AllMatrixUsersDataSourceTest {
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
result = Result.failure(Throwable("Ruhroh"))
|
result = Result.failure(Throwable("Ruhroh"))
|
||||||
)
|
)
|
||||||
val dataSource = AllMatrixUsersDataSource(matrixClient)
|
val dataSource = MatrixUserListDataSource(matrixClient)
|
||||||
|
|
||||||
val result = dataSource.getProfile(A_USER_ID)
|
val result = dataSource.getProfile(A_USER_ID)
|
||||||
Truth.assertThat(result).isNull()
|
Truth.assertThat(result).isNull()
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.usersearch.impl
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
internal class MatrixUserRepositoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - emits nothing if the search query is too short`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search("x")
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - returns empty list if no results are found`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search("some query")
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
assertThat(awaitItem()).isEmpty()
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - returns users if results are found`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(aMatrixUserList())
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search("some query")
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(aMatrixUserList())
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - immediately returns placeholder if search is mxid`() = runTest {
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)))
|
||||||
|
skipItems(1)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - does not change results if they contain searched mxid`() = runTest {
|
||||||
|
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID) + MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(searchResults)
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(searchResults)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - gets profile results if searched mxid not in results`() = runTest {
|
||||||
|
val userProfile = MatrixUser(userId = A_USER_ID, displayName = A_USER_NAME)
|
||||||
|
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||||
|
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(searchResults)
|
||||||
|
dataSource.givenUserProfile(userProfile)
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search - just shows id if profile can't be loaded`() = runTest {
|
||||||
|
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
|
||||||
|
|
||||||
|
val dataSource = FakeUserListDataSource()
|
||||||
|
dataSource.givenSearchResult(searchResults)
|
||||||
|
dataSource.givenUserProfile(null)
|
||||||
|
val repository = MatrixUserRepository(dataSource)
|
||||||
|
|
||||||
|
val result = repository.search(A_USER_ID.value)
|
||||||
|
|
||||||
|
result.test {
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults)
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -13,18 +13,18 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.userlist.test"
|
namespace = "io.element.android.libraries.usersearch"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
api(projects.features.userlist.api)
|
api(projects.libraries.usersearch.api)
|
||||||
api(libs.coroutines.core)
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.libraries.usersearch.test
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserListDataSource
|
||||||
|
|
||||||
class FakeUserListDataSource : UserListDataSource {
|
class FakeUserListDataSource : UserListDataSource {
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.usersearch.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
|
class FakeUserRepository : UserRepository {
|
||||||
|
|
||||||
|
var providedQuery: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val flow = MutableSharedFlow<List<MatrixUser>>()
|
||||||
|
|
||||||
|
override suspend fun search(query: String): Flow<List<MatrixUser>> {
|
||||||
|
providedQuery = query
|
||||||
|
return flow
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun emitResult(result: List<MatrixUser>) {
|
||||||
|
flow.emit(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -43,9 +43,7 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) {
|
||||||
androidTestImplementation(composeBom)
|
androidTestImplementation(composeBom)
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.material:material")
|
implementation("androidx.compose.material:material")
|
||||||
// Override BOM version, SearchBar is not available in the actual version
|
implementation("androidx.compose.material3:material3")
|
||||||
// do not use latest version because of clashes on androidx lifecycle dependency
|
|
||||||
implementation("androidx.compose.material3:material3:1.1.0-alpha04")
|
|
||||||
implementation("androidx.compose.material:material-icons-extended")
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
@ -92,6 +90,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:statemachine"))
|
implementation(project(":libraries:statemachine"))
|
||||||
implementation(project(":libraries:mediapickers:impl"))
|
implementation(project(":libraries:mediapickers:impl"))
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
|
implementation(project(":libraries:usersearch:impl"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.allServicesImpl() {
|
fun DependencyHandlerScope.allServicesImpl() {
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.samples.minimal
|
package io.element.android.samples.minimal
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.tests.testutils
|
package io.element.android.tests.testutils
|
||||||
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.TestCoroutineScheduler
|
import kotlinx.coroutines.test.TestCoroutineScheduler
|
||||||
import kotlinx.coroutines.test.TestDispatcher
|
import kotlinx.coroutines.test.TestDispatcher
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
|
|
||||||
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