ganfra
2 years ago
108 changed files with 1590 additions and 383 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
[Create and join rooms] Select members before creating a room (UI for selection) |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Design fixes for the session verification flow and button components. |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
/* |
||||
* 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.addpeople |
||||
|
||||
sealed interface AddPeopleEvents |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* 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.addpeople |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.libraries.di.SessionScope |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class AddPeopleNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
private val presenter: AddPeoplePresenter, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
AddPeopleView( |
||||
state = state, |
||||
modifier = modifier, |
||||
onBackPressed = { navigateUp() }, |
||||
onNextPressed = { }, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* 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.addpeople |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import io.element.android.features.selectusers.api.SelectUsersPresenter |
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs |
||||
import io.element.android.features.selectusers.api.SelectionMode |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import javax.inject.Inject |
||||
|
||||
class AddPeoplePresenter @Inject constructor( |
||||
private val selectUsersPresenterFactory: SelectUsersPresenter.Factory, |
||||
) : Presenter<AddPeopleState> { |
||||
|
||||
private val selectUsersPresenter by lazy { |
||||
selectUsersPresenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Multiple)) |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): AddPeopleState { |
||||
val selectUsersState = selectUsersPresenter.present() |
||||
|
||||
fun handleEvents(event: AddPeopleEvents) { |
||||
// do nothing for now |
||||
} |
||||
|
||||
return AddPeopleState( |
||||
selectUsersState = selectUsersState, |
||||
eventSink = ::handleEvents, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
/* |
||||
* 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.addpeople |
||||
|
||||
import io.element.android.features.selectusers.api.SelectUsersState |
||||
|
||||
data class AddPeopleState( |
||||
val selectUsersState: SelectUsersState, |
||||
val eventSink: (AddPeopleEvents) -> Unit, |
||||
) |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* 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.addpeople |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.features.selectusers.api.SelectionMode |
||||
import io.element.android.features.selectusers.api.aListOfSelectedUsers |
||||
import io.element.android.features.selectusers.api.aSelectUsersState |
||||
|
||||
open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> { |
||||
override val values: Sequence<AddPeopleState> |
||||
get() = sequenceOf( |
||||
aAddPeopleState(), |
||||
aAddPeopleState().copy( |
||||
selectUsersState = aSelectUsersState().copy( |
||||
selectedUsers = aListOfSelectedUsers(), |
||||
selectionMode = SelectionMode.Multiple, |
||||
) |
||||
), |
||||
aAddPeopleState().copy( |
||||
selectUsersState = aSelectUsersState().copy( |
||||
selectedUsers = aListOfSelectedUsers(), |
||||
isSearchActive = true, |
||||
selectionMode = SelectionMode.Multiple, |
||||
) |
||||
) |
||||
) |
||||
} |
||||
|
||||
fun aAddPeopleState() = AddPeopleState( |
||||
selectUsersState = aSelectUsersState(), |
||||
eventSink = {} |
||||
) |
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
/* |
||||
* Copyright (c) 2022 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.createroom.impl.addpeople |
||||
|
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
import androidx.compose.ui.tooling.preview.Preview |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.unit.sp |
||||
import io.element.android.features.selectusers.api.SelectUsersView |
||||
import io.element.android.libraries.designsystem.components.button.BackButton |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight |
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar |
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.designsystem.theme.components.TextButton |
||||
import io.element.android.libraries.ui.strings.R as StringR |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
fun AddPeopleView( |
||||
state: AddPeopleState, |
||||
modifier: Modifier = Modifier, |
||||
onBackPressed: () -> Unit = {}, |
||||
onNextPressed: () -> Unit = {}, |
||||
) { |
||||
val eventSink = state.eventSink |
||||
|
||||
Scaffold( |
||||
topBar = { |
||||
if (!state.selectUsersState.isSearchActive) { |
||||
AddPeopleViewTopBar( |
||||
hasSelectedUsers = state.selectUsersState.selectedUsers.isNotEmpty(), |
||||
onBackPressed = onBackPressed, |
||||
onNextPressed = onNextPressed, |
||||
) |
||||
} |
||||
} |
||||
) { padding -> |
||||
Column( |
||||
modifier = modifier |
||||
.fillMaxSize() |
||||
.padding(padding), |
||||
) { |
||||
SelectUsersView( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
state = state.selectUsersState, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
fun AddPeopleViewTopBar( |
||||
hasSelectedUsers: Boolean, |
||||
modifier: Modifier = Modifier, |
||||
onBackPressed: () -> Unit = {}, |
||||
onNextPressed: () -> Unit = {}, |
||||
) { |
||||
CenterAlignedTopAppBar( |
||||
modifier = modifier, |
||||
title = { |
||||
Text( |
||||
text = stringResource(id = StringR.string.add_people), |
||||
fontSize = 16.sp, |
||||
fontWeight = FontWeight.SemiBold, |
||||
) |
||||
}, |
||||
navigationIcon = { BackButton(onClick = onBackPressed) }, |
||||
actions = { |
||||
TextButton( |
||||
modifier = Modifier.padding(horizontal = 8.dp), |
||||
onClick = onNextPressed, |
||||
) { |
||||
val textActionResId = if (hasSelectedUsers) StringR.string.action_next else StringR.string.action_skip |
||||
Text( |
||||
text = stringResource(id = textActionResId), |
||||
fontSize = 16.sp, |
||||
) |
||||
} |
||||
} |
||||
) |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) = |
||||
ElementPreviewLight { ContentToPreview(state) } |
||||
|
||||
@Preview |
||||
@Composable |
||||
internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) = |
||||
ElementPreviewDark { ContentToPreview(state) } |
||||
|
||||
@Composable |
||||
private fun ContentToPreview(state: AddPeopleState) { |
||||
AddPeopleView(state = state) |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* 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(ExperimentalCoroutinesApi::class) |
||||
|
||||
package io.element.android.features.createroom.impl.addpeople |
||||
|
||||
import app.cash.molecule.RecompositionClock |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.test |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs |
||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Before |
||||
import org.junit.Test |
||||
|
||||
class AddPeoplePresenterTests { |
||||
|
||||
private lateinit var presenter: AddPeoplePresenter |
||||
|
||||
@Before |
||||
fun setup() { |
||||
val selectUsersFactory = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory { |
||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args) |
||||
} |
||||
presenter = AddPeoplePresenter(selectUsersFactory) |
||||
} |
||||
|
||||
@Test |
||||
fun `present - initial state`() = runTest { |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-compose-library") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.selectusers.api" |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(projects.libraries.architecture) |
||||
implementation(projects.libraries.designsystem) |
||||
implementation(projects.libraries.uiStrings) |
||||
implementation(projects.libraries.matrix.api) |
||||
implementation(projects.libraries.matrixui) |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* 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.selectusers.api |
||||
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser |
||||
|
||||
sealed interface SelectUsersEvents { |
||||
data class UpdateSearchQuery(val query: String) : SelectUsersEvents |
||||
data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents |
||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents |
||||
data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* 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.selectusers.api |
||||
|
||||
import io.element.android.libraries.architecture.Presenter |
||||
|
||||
interface SelectUsersPresenter : Presenter<SelectUsersState> { |
||||
|
||||
interface Factory { |
||||
fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* 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.selectusers.api |
||||
|
||||
data class SelectUsersPresenterArgs( |
||||
val selectionMode: SelectionMode, |
||||
) |
||||
|
||||
enum class SelectionMode { |
||||
Single, |
||||
Multiple, |
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* 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.selectusers.api |
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser |
||||
import kotlinx.collections.immutable.ImmutableList |
||||
|
||||
data class SelectUsersState( |
||||
val searchQuery: String, |
||||
val searchResults: ImmutableList<MatrixUser>, |
||||
val selectedUsers: ImmutableList<MatrixUser>, |
||||
val selectedUsersListState: LazyListState, |
||||
val isSearchActive: Boolean, |
||||
val selectionMode: SelectionMode, |
||||
val eventSink: (SelectUsersEvents) -> Unit, |
||||
) { |
||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple |
||||
} |
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
/* |
||||
* 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.selectusers.api |
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser |
||||
import kotlinx.collections.immutable.persistentListOf |
||||
|
||||
open class SelectUsersStateProvider : PreviewParameterProvider<SelectUsersState> { |
||||
override val values: Sequence<SelectUsersState> |
||||
get() = sequenceOf( |
||||
aSelectUsersState(), |
||||
aSelectUsersState().copy( |
||||
isSearchActive = false, |
||||
selectedUsers = aListOfSelectedUsers(), |
||||
selectionMode = SelectionMode.Multiple, |
||||
), |
||||
aSelectUsersState().copy(isSearchActive = true), |
||||
aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone"), |
||||
aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), |
||||
aSelectUsersState().copy( |
||||
isSearchActive = true, |
||||
searchQuery = "@someone:matrix.org", |
||||
selectedUsers = aListOfSelectedUsers(), |
||||
searchResults = aListOfResults(), |
||||
), |
||||
aSelectUsersState().copy( |
||||
isSearchActive = true, |
||||
searchQuery = "@someone:matrix.org", |
||||
selectionMode = SelectionMode.Multiple, |
||||
selectedUsers = aListOfSelectedUsers(), |
||||
searchResults = aListOfResults(), |
||||
) |
||||
) |
||||
} |
||||
|
||||
fun aSelectUsersState() = SelectUsersState( |
||||
isSearchActive = false, |
||||
searchQuery = "", |
||||
searchResults = persistentListOf(), |
||||
selectedUsers = persistentListOf(), |
||||
selectedUsersListState = LazyListState( |
||||
firstVisibleItemIndex = 0, |
||||
firstVisibleItemScrollOffset = 0, |
||||
), |
||||
selectionMode = SelectionMode.Single, |
||||
eventSink = {} |
||||
) |
||||
|
||||
fun aListOfSelectedUsers() = persistentListOf( |
||||
MatrixUser(id = UserId("@someone:matrix.org")), |
||||
MatrixUser(id = UserId("@other:matrix.org"), username = "other"), |
||||
) |
||||
|
||||
fun aListOfResults() = persistentListOf( |
||||
MatrixUser(id = UserId("@someone:matrix.org")), |
||||
MatrixUser(id = UserId("@other:matrix.org"), username = "other"), |
||||
MatrixUser( |
||||
id = UserId("@someone_with_a_very_long_matrix_identifier:a_very_long_domain.org"), |
||||
username = "hey, I am someone with a very long display name" |
||||
), |
||||
MatrixUser(id = UserId("@someone_2:matrix.org"), username = "someone 2"), |
||||
MatrixUser(id = UserId("@someone_3:matrix.org"), username = "someone 3"), |
||||
MatrixUser(id = UserId("@someone_4:matrix.org"), username = "someone 4"), |
||||
MatrixUser(id = UserId("@someone_5:matrix.org"), username = "someone 5"), |
||||
MatrixUser(id = UserId("@someone_6:matrix.org"), username = "someone 6"), |
||||
MatrixUser(id = UserId("@someone_7:matrix.org"), username = "someone 7"), |
||||
MatrixUser(id = UserId("@someone_8:matrix.org"), username = "someone 8"), |
||||
MatrixUser(id = UserId("@someone_9:matrix.org"), username = "someone 9"), |
||||
MatrixUser(id = UserId("@someone_10:matrix.org"), username = "someone 10"), |
||||
) |
@ -0,0 +1,311 @@
@@ -0,0 +1,311 @@
|
||||
/* |
||||
* 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.selectusers.api |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import androidx.compose.foundation.lazy.LazyRow |
||||
import androidx.compose.foundation.lazy.items |
||||
import androidx.compose.foundation.shape.CircleShape |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.filled.Close |
||||
import androidx.compose.material.icons.filled.Search |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
import androidx.compose.material3.MaterialTheme |
||||
import androidx.compose.material3.SearchBarDefaults |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.alpha |
||||
import androidx.compose.ui.draw.clip |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.platform.LocalFocusManager |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.tooling.preview.Preview |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.components.button.BackButton |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight |
||||
import io.element.android.libraries.designsystem.theme.components.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.ui.components.CheckableMatrixUserRow |
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow |
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser |
||||
import io.element.android.libraries.matrix.ui.model.getBestName |
||||
import kotlinx.collections.immutable.ImmutableList |
||||
import io.element.android.libraries.ui.strings.R as StringR |
||||
|
||||
@Composable |
||||
fun SelectUsersView( |
||||
state: SelectUsersState, |
||||
modifier: Modifier = Modifier, |
||||
onUserSelected: (MatrixUser) -> Unit = {}, |
||||
onUserDeselected: (MatrixUser) -> Unit = {}, |
||||
) { |
||||
Column( |
||||
modifier = modifier, |
||||
) { |
||||
SearchUserBar( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
query = state.searchQuery, |
||||
results = state.searchResults, |
||||
selectedUsers = state.selectedUsers, |
||||
selectedUsersListState = state.selectedUsersListState, |
||||
active = state.isSearchActive, |
||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled, |
||||
onActiveChanged = { state.eventSink(SelectUsersEvents.OnSearchActiveChanged(it)) }, |
||||
onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) }, |
||||
onUserSelected = { |
||||
state.eventSink(SelectUsersEvents.AddToSelection(it)) |
||||
onUserSelected(it) |
||||
}, |
||||
onUserDeselected = { |
||||
state.eventSink(SelectUsersEvents.RemoveFromSelection(it)) |
||||
onUserDeselected(it) |
||||
}, |
||||
) |
||||
|
||||
if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { |
||||
SelectedUsersList( |
||||
listState = state.selectedUsersListState, |
||||
modifier = Modifier.padding(16.dp), |
||||
selectedUsers = state.selectedUsers, |
||||
onUserRemoved = { |
||||
state.eventSink(SelectUsersEvents.RemoveFromSelection(it)) |
||||
onUserDeselected(it) |
||||
}, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class) |
||||
@Composable |
||||
fun SearchUserBar( |
||||
query: String, |
||||
results: ImmutableList<MatrixUser>, |
||||
selectedUsers: ImmutableList<MatrixUser>, |
||||
selectedUsersListState: LazyListState, |
||||
active: Boolean, |
||||
isMultiSelectionEnabled: Boolean, |
||||
modifier: Modifier = Modifier, |
||||
placeHolderTitle: String = stringResource(StringR.string.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(StringR.string.a11y_clear)) |
||||
} |
||||
} |
||||
} |
||||
!active -> { |
||||
{ |
||||
Icon( |
||||
imageVector = Icons.Default.Search, |
||||
contentDescription = stringResource(StringR.string.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( |
||||
listState = selectedUsersListState, |
||||
modifier = Modifier.padding(16.dp), |
||||
selectedUsers = selectedUsers, |
||||
onUserRemoved = onUserDeselected, |
||||
) |
||||
} |
||||
|
||||
LazyColumn { |
||||
if (isMultiSelectionEnabled) { |
||||
items(results) { matrixUser -> |
||||
SearchMultipleUsersResultItem( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
matrixUser = matrixUser, |
||||
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null, |
||||
onCheckedChange = { checked -> |
||||
if (checked) { |
||||
onUserSelected(matrixUser) |
||||
} else { |
||||
onUserDeselected(matrixUser) |
||||
} |
||||
} |
||||
) |
||||
} |
||||
} else { |
||||
items(results) { matrixUser -> |
||||
SearchSingleUserResultItem( |
||||
modifier = Modifier.fillMaxWidth(), |
||||
matrixUser = matrixUser, |
||||
onClick = { onUserSelected(matrixUser) } |
||||
) |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
fun SearchMultipleUsersResultItem( |
||||
matrixUser: MatrixUser, |
||||
isUserSelected: Boolean, |
||||
modifier: Modifier = Modifier, |
||||
onCheckedChange: (Boolean) -> Unit, |
||||
) { |
||||
CheckableMatrixUserRow( |
||||
checked = isUserSelected, |
||||
modifier = modifier, |
||||
matrixUser = matrixUser, |
||||
avatarSize = AvatarSize.Custom(36.dp), |
||||
onCheckedChange = onCheckedChange, |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
fun SearchSingleUserResultItem( |
||||
matrixUser: MatrixUser, |
||||
modifier: Modifier = Modifier, |
||||
onClick: () -> Unit = {}, |
||||
) { |
||||
MatrixUserRow( |
||||
modifier = modifier.clickable(onClick = onClick), |
||||
matrixUser = matrixUser, |
||||
avatarSize = AvatarSize.Custom(36.dp), |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
fun SelectedUsersList( |
||||
listState: LazyListState, |
||||
selectedUsers: ImmutableList<MatrixUser>, |
||||
modifier: Modifier = Modifier, |
||||
onUserRemoved: (MatrixUser) -> Unit = {}, |
||||
) { |
||||
LazyRow( |
||||
state = listState, |
||||
modifier = modifier, |
||||
horizontalArrangement = Arrangement.spacedBy(24.dp), |
||||
) { |
||||
items(selectedUsers.toList()) { matrixUser -> |
||||
SelectedUser( |
||||
matrixUser = matrixUser, |
||||
onUserRemoved = onUserRemoved, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun SelectedUser( |
||||
matrixUser: MatrixUser, |
||||
modifier: Modifier = Modifier, |
||||
onUserRemoved: (MatrixUser) -> Unit, |
||||
) { |
||||
Box(modifier = modifier.width(56.dp)) { |
||||
Column( |
||||
horizontalAlignment = Alignment.CenterHorizontally, |
||||
) { |
||||
Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56.dp))) |
||||
Text( |
||||
text = matrixUser.getBestName(), |
||||
overflow = TextOverflow.Ellipsis, |
||||
maxLines = 1, |
||||
style = MaterialTheme.typography.bodyLarge, |
||||
) |
||||
} |
||||
IconButton( |
||||
modifier = Modifier |
||||
.clip(CircleShape) |
||||
.background(MaterialTheme.colorScheme.primary) |
||||
.size(20.dp) |
||||
.align(Alignment.TopEnd), |
||||
onClick = { onUserRemoved(matrixUser) } |
||||
) { |
||||
Icon( |
||||
imageVector = Icons.Default.Close, |
||||
contentDescription = stringResource(id = StringR.string.action_remove), |
||||
tint = MaterialTheme.colorScheme.onPrimary, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
internal fun SelectUsersViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = |
||||
ElementPreviewLight { ContentToPreview(state) } |
||||
|
||||
@Preview |
||||
@Composable |
||||
internal fun SelectUsersViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) = |
||||
ElementPreviewDark { ContentToPreview(state) } |
||||
|
||||
@Composable |
||||
private fun ContentToPreview(state: SelectUsersState) { |
||||
SelectUsersView(state = state) |
||||
} |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
/* |
||||
* Copyright (c) 2022 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed |
||||
@Suppress("DSL_SCOPE_VIOLATION") |
||||
plugins { |
||||
id("io.element.android-compose-library") |
||||
alias(libs.plugins.anvil) |
||||
alias(libs.plugins.ksp) |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.selectusers.impl" |
||||
} |
||||
|
||||
anvil { |
||||
generateDaggerFactories.set(true) |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(projects.anvilannotations) |
||||
anvil(projects.anvilcodegen) |
||||
implementation(projects.libraries.core) |
||||
implementation(projects.libraries.architecture) |
||||
implementation(projects.libraries.matrix.api) |
||||
implementation(projects.libraries.matrixui) |
||||
implementation(projects.libraries.designsystem) |
||||
implementation(projects.libraries.elementresources) |
||||
implementation(projects.libraries.testtags) |
||||
implementation(projects.libraries.uiStrings) |
||||
api(projects.features.selectusers.api) |
||||
ksp(libs.showkase.processor) |
||||
|
||||
testImplementation(libs.test.junit) |
||||
testImplementation(libs.coroutines.test) |
||||
testImplementation(libs.coroutines.core) |
||||
testImplementation(libs.molecule.runtime) |
||||
testImplementation(libs.test.truth) |
||||
testImplementation(libs.test.turbine) |
||||
testImplementation(libs.test.mockk) |
||||
testImplementation(projects.libraries.matrix.test) |
||||
|
||||
androidTestImplementation(libs.test.junitext) |
||||
} |
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* 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.selectusers.impl |
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import androidx.compose.foundation.lazy.rememberLazyListState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.runtime.MutableState |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.runtime.setValue |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedFactory |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.features.selectusers.api.SelectUsersEvents |
||||
import io.element.android.features.selectusers.api.SelectUsersPresenter |
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs |
||||
import io.element.android.features.selectusers.api.SelectUsersState |
||||
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.ui.model.MatrixUser |
||||
import kotlinx.collections.immutable.ImmutableList |
||||
import kotlinx.collections.immutable.persistentListOf |
||||
import kotlinx.collections.immutable.toImmutableList |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
|
||||
class DefaultSelectUsersPresenter @AssistedInject constructor( |
||||
@Assisted val args: SelectUsersPresenterArgs, |
||||
) : SelectUsersPresenter { |
||||
|
||||
@AssistedFactory |
||||
@ContributesBinding(SessionScope::class) |
||||
interface DefaultSelectUsersFactory : SelectUsersPresenter.Factory { |
||||
override fun create(args: SelectUsersPresenterArgs): DefaultSelectUsersPresenter |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): SelectUsersState { |
||||
val localCoroutineScope = rememberCoroutineScope() |
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) } |
||||
val selectedUsers: MutableState<ImmutableList<MatrixUser>> = remember { |
||||
mutableStateOf(persistentListOf()) |
||||
} |
||||
val selectedUsersListState = rememberLazyListState() |
||||
var searchQuery by rememberSaveable { mutableStateOf("") } |
||||
val searchResults: MutableState<ImmutableList<MatrixUser>> = remember { |
||||
mutableStateOf(persistentListOf()) |
||||
} |
||||
|
||||
fun handleEvents(event: SelectUsersEvents) { |
||||
when (event) { |
||||
is SelectUsersEvents.OnSearchActiveChanged -> isSearchActive = event.active |
||||
is SelectUsersEvents.UpdateSearchQuery -> searchQuery = event.query |
||||
is SelectUsersEvents.AddToSelection -> { |
||||
if (event.matrixUser !in selectedUsers.value) { |
||||
selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList() |
||||
} |
||||
localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) |
||||
} |
||||
is SelectUsersEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() |
||||
} |
||||
} |
||||
|
||||
LaunchedEffect(searchQuery) { |
||||
// Clear the search results before performing the search, manually add a fake result with the matrixId, if any |
||||
searchResults.value = if (MatrixPatterns.isUserId(searchQuery)) { |
||||
persistentListOf(MatrixUser(UserId(searchQuery))) |
||||
} else { |
||||
persistentListOf() |
||||
} |
||||
// Perform the search asynchronously |
||||
if (searchQuery.isNotEmpty()) { |
||||
searchResults.value = performSearch(searchQuery) |
||||
} |
||||
} |
||||
|
||||
return SelectUsersState( |
||||
searchQuery = searchQuery, |
||||
searchResults = searchResults.value, |
||||
selectedUsers = selectedUsers.value.reversed().toImmutableList(), |
||||
selectedUsersListState = selectedUsersListState, |
||||
isSearchActive = isSearchActive, |
||||
selectionMode = args.selectionMode, |
||||
eventSink = ::handleEvents, |
||||
) |
||||
} |
||||
|
||||
private fun performSearch(query: String): ImmutableList<MatrixUser> { |
||||
val isMatrixId = MatrixPatterns.isUserId(query) |
||||
val results = mutableListOf<MatrixUser>()// TODO trigger /search request |
||||
if (isMatrixId && results.none { it.id.value == query }) { |
||||
val getProfileResult: MatrixUser? = null // TODO trigger /profile request |
||||
val profile = getProfileResult ?: MatrixUser(UserId(query)) |
||||
results.add(0, profile) |
||||
} |
||||
return results.toImmutableList() |
||||
} |
||||
|
||||
private fun CoroutineScope.scrollToFirstSelectedUser(listState: LazyListState) = launch { |
||||
listState.scrollToItem(index = 0) |
||||
} |
||||
} |
@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
|
||||
/* |
||||
* 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.selectusers.impl |
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import app.cash.molecule.RecompositionClock |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.test |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.selectusers.api.SelectUsersEvents |
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs |
||||
import io.element.android.features.selectusers.api.SelectionMode |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser |
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser |
||||
import io.mockk.coJustRun |
||||
import io.mockk.mockkConstructor |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) |
||||
class DefaultSelectUsersPresenterTests { |
||||
|
||||
@Test |
||||
fun `present - initial state for single selection`() = runTest { |
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.searchQuery).isEmpty() |
||||
assertThat(initialState.isMultiSelectionEnabled).isFalse() |
||||
assertThat(initialState.isSearchActive).isFalse() |
||||
assertThat(initialState.selectedUsers).isEmpty() |
||||
assertThat(initialState.searchResults).isEmpty() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - initial state for multiple selection`() = runTest { |
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Multiple)) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.searchQuery).isEmpty() |
||||
assertThat(initialState.isMultiSelectionEnabled).isTrue() |
||||
assertThat(initialState.isSearchActive).isFalse() |
||||
assertThat(initialState.selectedUsers).isEmpty() |
||||
assertThat(initialState.searchResults).isEmpty() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - update search query`() = runTest { |
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
|
||||
initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(true)) |
||||
assertThat(awaitItem().isSearchActive).isTrue() |
||||
|
||||
val matrixIdQuery = "@name:matrix.org" |
||||
initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(matrixIdQuery)) |
||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery) |
||||
assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery))) |
||||
|
||||
val notMatrixIdQuery = "name" |
||||
initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(notMatrixIdQuery)) |
||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery) |
||||
assertThat(awaitItem().searchResults).isEmpty() |
||||
|
||||
initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(false)) |
||||
assertThat(awaitItem().isSearchActive).isFalse() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - select a user`() = runTest { |
||||
mockkConstructor(LazyListState::class) |
||||
coJustRun { anyConstructed<LazyListState>().scrollToItem(index = any()) } |
||||
|
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single)) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
|
||||
val userA = aMatrixUser("userA", "A") |
||||
val userB = aMatrixUser("userB", "B") |
||||
val userABis = aMatrixUser("userA", "A") |
||||
val userC = aMatrixUser("userC", "C") |
||||
|
||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userA)) |
||||
assertThat(awaitItem().selectedUsers).containsExactly(userA) |
||||
|
||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userB)) |
||||
// the last added user should be presented first |
||||
assertThat(awaitItem().selectedUsers).containsExactly(userB, userA) |
||||
|
||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userABis)) |
||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userC)) |
||||
// duplicated users should be ignored |
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA) |
||||
|
||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userB)) |
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userA) |
||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userA)) |
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC) |
||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userC)) |
||||
assertThat(awaitItem().selectedUsers).isEmpty() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
/* |
||||
* 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.matrix.ui.components |
||||
|
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.semantics.Role |
||||
import androidx.compose.ui.tooling.preview.Preview |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark |
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight |
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox |
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser |
||||
|
||||
@Composable |
||||
fun CheckableMatrixUserRow( |
||||
checked: Boolean, |
||||
matrixUser: MatrixUser, |
||||
modifier: Modifier = Modifier, |
||||
avatarSize: AvatarSize = matrixUser.avatarData.size, |
||||
onCheckedChange: (Boolean) -> Unit = {}, |
||||
enabled: Boolean = true, |
||||
) { |
||||
|
||||
Row( |
||||
modifier = modifier |
||||
.fillMaxWidth() |
||||
.clickable(role = Role.Checkbox) { onCheckedChange(!checked) }, |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
) { |
||||
MatrixUserRow( |
||||
modifier = Modifier.weight(1f), |
||||
matrixUser = matrixUser, |
||||
avatarSize = avatarSize, |
||||
) |
||||
|
||||
Checkbox( |
||||
checked = checked, |
||||
onCheckedChange = onCheckedChange, |
||||
enabled = enabled, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
internal fun CheckableMatrixUserRowLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = |
||||
ElementPreviewLight { ContentToPreview(matrixUser) } |
||||
|
||||
@Preview |
||||
@Composable |
||||
internal fun CheckableMatrixUserRowDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = |
||||
ElementPreviewDark { ContentToPreview(matrixUser) } |
||||
|
||||
@Composable |
||||
private fun ContentToPreview(matrixUser: MatrixUser) { |
||||
Column { |
||||
CheckableMatrixUserRow(checked = true, matrixUser) |
||||
CheckableMatrixUserRow(checked = false, matrixUser) |
||||
} |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
/* |
||||
* Copyright (c) 2022 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed |
||||
@Suppress("DSL_SCOPE_VIOLATION") |
||||
plugins { |
||||
id("java-library") |
||||
id("com.android.lint") |
||||
alias(libs.plugins.kotlin.jvm) |
||||
} |
||||
|
||||
java { |
||||
sourceCompatibility = JavaVersion.VERSION_1_8 |
||||
targetCompatibility = JavaVersion.VERSION_1_8 |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(libs.coroutines.core) |
||||
|
||||
testImplementation(libs.test.junit) |
||||
testImplementation(libs.test.truth) |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue