[Room member list] Display room member list (#276)
* Implement room member list * Move timeline initialization back to `TimelinePresenter`. * Fix crash when the `innerRoom` inside a `RustMatrixRoom` is destroyed but `syncUpdateFlow` is still running. * Address review comments
This commit is contained in:
committed by
GitHub
parent
f0b95d30be
commit
d7a6779343
@@ -47,7 +47,7 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.selectusers.api)
|
||||
implementation(projects.features.userlist.api)
|
||||
api(projects.features.createroom.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
@@ -56,7 +56,8 @@ dependencies {
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.selectusers.impl)
|
||||
testImplementation(projects.features.userlist.impl)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
|
||||
@@ -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.createroom.impl
|
||||
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO this is empty as we currently don't have an endpoint to perform user search
|
||||
class AllMatrixUsersDataSource @Inject constructor() : MatrixUserDataSource {
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -17,31 +17,38 @@
|
||||
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.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class AddPeoplePresenter @Inject constructor(
|
||||
private val selectUsersPresenterFactory: SelectUsersPresenter.Factory,
|
||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
) : Presenter<AddPeopleState> {
|
||||
|
||||
private val selectUsersPresenter by lazy {
|
||||
selectUsersPresenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Multiple))
|
||||
private val userListPresenter by lazy {
|
||||
userListPresenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||
matrixUserDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): AddPeopleState {
|
||||
val selectUsersState = selectUsersPresenter.present()
|
||||
val userListState = userListPresenter.present()
|
||||
|
||||
fun handleEvents(event: AddPeopleEvents) {
|
||||
// do nothing for now
|
||||
}
|
||||
|
||||
return AddPeopleState(
|
||||
selectUsersState = selectUsersState,
|
||||
userListState = userListState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
import io.element.android.features.selectusers.api.SelectUsersState
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
|
||||
data class AddPeopleState(
|
||||
val selectUsersState: SelectUsersState,
|
||||
val userListState: UserListState,
|
||||
val eventSink: (AddPeopleEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -17,22 +17,22 @@
|
||||
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
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
|
||||
open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
||||
override val values: Sequence<AddPeopleState>
|
||||
get() = sequenceOf(
|
||||
aAddPeopleState(),
|
||||
aAddPeopleState().copy(
|
||||
selectUsersState = aSelectUsersState().copy(
|
||||
userListState = aUserListState().copy(
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
)
|
||||
),
|
||||
aAddPeopleState().copy(
|
||||
selectUsersState = aSelectUsersState().copy(
|
||||
userListState = aUserListState().copy(
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
isSearchActive = true,
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
@@ -42,6 +42,6 @@ open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
||||
}
|
||||
|
||||
fun aAddPeopleState() = AddPeopleState(
|
||||
selectUsersState = aSelectUsersState(),
|
||||
userListState = aUserListState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
@@ -29,8 +29,8 @@ 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.userlist.api.UserListView
|
||||
import io.element.android.features.createroom.impl.R
|
||||
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
|
||||
@@ -52,9 +52,9 @@ fun AddPeopleView(
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!state.selectUsersState.isSearchActive) {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
AddPeopleViewTopBar(
|
||||
hasSelectedUsers = state.selectUsersState.selectedUsers.isNotEmpty(),
|
||||
hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(),
|
||||
onBackPressed = onBackPressed,
|
||||
onNextPressed = onNextPressed,
|
||||
)
|
||||
@@ -66,9 +66,9 @@ fun AddPeopleView(
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
SelectUsersView(
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.selectUsersState,
|
||||
state = state.userListState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
interface CreateRoomModule {
|
||||
|
||||
@Binds
|
||||
@Named("AllUsers")
|
||||
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): MatrixUserDataSource
|
||||
|
||||
}
|
||||
@@ -17,25 +17,31 @@
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
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.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class CreateRoomRootPresenter @Inject constructor(
|
||||
private val presenterFactory: SelectUsersPresenter.Factory,
|
||||
private val presenterFactory: UserListPresenter.Factory,
|
||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
) : Presenter<CreateRoomRootState> {
|
||||
|
||||
private val presenter by lazy {
|
||||
presenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Single))
|
||||
presenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
matrixUserDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): CreateRoomRootState {
|
||||
val selectUsersState = presenter.present()
|
||||
val userListState = presenter.present()
|
||||
|
||||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
@@ -45,7 +51,7 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
return CreateRoomRootState(
|
||||
selectUsersState = selectUsersState,
|
||||
userListState = userListState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import io.element.android.features.selectusers.api.SelectUsersState
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
|
||||
data class CreateRoomRootState(
|
||||
val selectUsersState: SelectUsersState,
|
||||
val userListState: UserListState,
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.selectusers.api.aSelectUsersState
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
|
||||
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
||||
override val values: Sequence<CreateRoomRootState>
|
||||
@@ -28,5 +28,5 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||
|
||||
fun aCreateRoomRootState() = CreateRoomRootState(
|
||||
eventSink = {},
|
||||
selectUsersState = aSelectUsersState(),
|
||||
userListState = aUserListState(),
|
||||
)
|
||||
|
||||
@@ -37,8 +37,8 @@ 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.userlist.api.UserListView
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.selectusers.api.SelectUsersView
|
||||
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
|
||||
@@ -60,7 +60,7 @@ fun CreateRoomRootView(
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
topBar = {
|
||||
if (!state.selectUsersState.isSearchActive) {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
CreateRoomRootViewTopBar(onClosePressed = onClosePressed)
|
||||
}
|
||||
}
|
||||
@@ -69,13 +69,13 @@ fun CreateRoomRootView(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
SelectUsersView(
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state.selectUsersState,
|
||||
state = state.userListState,
|
||||
onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) },
|
||||
)
|
||||
|
||||
if (!state.selectUsersState.isSearchActive) {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
CreateRoomActionButtonsList(
|
||||
onNewRoomClicked = onNewRoomClicked,
|
||||
onInvitePeopleClicked = { state.eventSink(CreateRoomRootEvents.InvitePeople) },
|
||||
|
||||
@@ -22,8 +22,10 @@ import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
@@ -35,10 +37,11 @@ class AddPeoplePresenterTests {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val selectUsersFactory = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
||||
val userListFactory = object : DefaultUserListPresenter.DefaultUserListFactory {
|
||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||
}
|
||||
presenter = AddPeoplePresenter(selectUsersFactory)
|
||||
val dataSource = FakeMatrixUserDataSource()
|
||||
presenter = AddPeoplePresenter(userListFactory, dataSource)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,3 +54,4 @@ class AddPeoplePresenterTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -33,14 +35,16 @@ import org.junit.Test
|
||||
|
||||
class CreateRoomRootPresenterTests {
|
||||
|
||||
private lateinit var userListDataSource: FakeMatrixUserDataSource
|
||||
private lateinit var presenter: CreateRoomRootPresenter
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
||||
val userListPresenter = object : DefaultUserListPresenter.DefaultUserListFactory {
|
||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||
}
|
||||
presenter = CreateRoomRootPresenter(selectUsersPresenter)
|
||||
userListDataSource = FakeMatrixUserDataSource()
|
||||
presenter = CreateRoomRootPresenter(userListPresenter, userListDataSource)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -42,6 +42,7 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.userlist.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
api(projects.features.roomdetails.api)
|
||||
implementation(libs.coil.compose)
|
||||
@@ -52,6 +53,8 @@ dependencies {
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.impl)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
@@ -49,11 +51,25 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomMemberList : NavTarget
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun openRoomMemberList()
|
||||
}
|
||||
|
||||
val callback = object : Callback {
|
||||
override fun openRoomMemberList() {
|
||||
backstack.push(NavTarget.RoomMemberList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.RoomDetails -> createNode<RoomDetailsNode>(buildContext)
|
||||
NavTarget.RoomDetails -> createNode<RoomDetailsNode>(buildContext, listOf(callback))
|
||||
NavTarget.RoomMemberList -> createNode<RoomMemberListNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
@@ -40,6 +41,12 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val callback = plugins<RoomDetailsFlowNode.Callback>().firstOrNull()
|
||||
|
||||
private fun openRoomMemberList() {
|
||||
callback?.openRoomMemberList()
|
||||
}
|
||||
|
||||
private fun onShareRoom(context: Context) {
|
||||
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
||||
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
|
||||
@@ -64,6 +71,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||
modifier = modifier,
|
||||
goBack = { navigateUp() },
|
||||
onShareRoom = { onShareRoom(context) },
|
||||
openRoomMemberList = ::openRoomMemberList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,16 @@
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomDetailsPresenter @Inject constructor(
|
||||
@@ -29,13 +37,24 @@ class RoomDetailsPresenter @Inject constructor(
|
||||
override fun present(): RoomDetailsState {
|
||||
// fun handleEvents(event: RoomDetailsEvent) {}
|
||||
|
||||
var memberCount: Async<Int> by remember { mutableStateOf(Async.Loading()) }
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
memberCount = runCatching { room.memberCount() }
|
||||
.fold(
|
||||
onSuccess = { Async.Success(it) },
|
||||
onFailure = { Async.Failure(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return RoomDetailsState(
|
||||
roomId = room.roomId.value,
|
||||
roomName = room.name ?: room.displayName,
|
||||
roomAlias = room.alias,
|
||||
roomAvatarUrl = room.avatarUrl,
|
||||
roomTopic = room.topic,
|
||||
memberCount = room.members.size,
|
||||
memberCount = memberCount,
|
||||
isEncrypted = room.isEncrypted,
|
||||
// eventSink = ::handleEvents
|
||||
)
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
data class RoomDetailsState(
|
||||
val roomId: String,
|
||||
val roomName: String,
|
||||
val roomAlias: String?,
|
||||
val roomAvatarUrl: String?,
|
||||
val roomTopic: String?,
|
||||
val memberCount: Int,
|
||||
val memberCount: Async<Int>,
|
||||
val isEncrypted: Boolean,
|
||||
// val eventSink: (RoomDetailsEvent) -> Unit
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
||||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||
override val values: Sequence<RoomDetailsState>
|
||||
@@ -25,6 +26,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||
aRoomDetailsState().copy(roomTopic = null),
|
||||
aRoomDetailsState().copy(isEncrypted = false),
|
||||
aRoomDetailsState().copy(roomAlias = null),
|
||||
aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
@@ -39,7 +41,7 @@ fun aRoomDetailsState() = RoomDetailsState(
|
||||
"|| MAIL iki/Marketing " +
|
||||
"|| MAI iki/Marketing " +
|
||||
"|| MAI iki/Marketing...",
|
||||
memberCount = 32,
|
||||
memberCount = Async.Success(32),
|
||||
isEncrypted = true,
|
||||
// eventSink = {}
|
||||
)
|
||||
|
||||
@@ -42,6 +42,8 @@ import androidx.compose.ui.res.vectorResource
|
||||
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.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
@@ -62,6 +64,7 @@ fun RoomDetailsView(
|
||||
state: RoomDetailsState,
|
||||
goBack: () -> Unit,
|
||||
onShareRoom: () -> Unit,
|
||||
openRoomMemberList: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -87,7 +90,12 @@ fun RoomDetailsView(
|
||||
TopicSection(roomTopic = state.roomTopic)
|
||||
}
|
||||
|
||||
MembersSection(memberCount = state.memberCount)
|
||||
val memberCount = (state.memberCount as? Async.Success<Int>)?.state
|
||||
MembersSection(
|
||||
memberCount = memberCount,
|
||||
isLoading = state.memberCount.isLoading(),
|
||||
openRoomMemberList = openRoomMemberList
|
||||
)
|
||||
|
||||
if (state.isEncrypted) {
|
||||
SecuritySection()
|
||||
@@ -148,12 +156,19 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MembersSection(memberCount: Int, modifier: Modifier = Modifier) {
|
||||
internal fun MembersSection(
|
||||
memberCount: Int?,
|
||||
isLoading: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
openRoomMemberList: () -> Unit
|
||||
) {
|
||||
PreferenceCategory(modifier = modifier) {
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_details_people_title),
|
||||
icon = Icons.Outlined.Person,
|
||||
currentValue = memberCount.toString(),
|
||||
currentValue = memberCount?.toString(),
|
||||
onClick = openRoomMemberList,
|
||||
loadingCurrentValue = isLoading,
|
||||
)
|
||||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_details_invite_people_title),
|
||||
@@ -200,5 +215,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
||||
state = state,
|
||||
goBack = {},
|
||||
onShareRoom = {},
|
||||
openRoomMemberList = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.roomdetails.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
interface RoomMemberModule {
|
||||
|
||||
@Binds
|
||||
@Named("RoomMembers")
|
||||
fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.roomdetails.impl.members
|
||||
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMatrixUserDataSource @Inject constructor(
|
||||
private val room: MatrixRoom
|
||||
) : MatrixUserDataSource {
|
||||
|
||||
override suspend fun search(query: String): List<MatrixUser> {
|
||||
return room.members().filter { member ->
|
||||
if (query.isBlank()) {
|
||||
true
|
||||
} else {
|
||||
member.userId.contains(query, ignoreCase = true) || member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||
}
|
||||
}.map(::mapMemberToMatrixUser)
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun mapMemberToMatrixUser(member: RoomMember): MatrixUser {
|
||||
return MatrixUser(
|
||||
id = UserId(member.userId),
|
||||
username = member.displayName,
|
||||
avatarData = AvatarData(
|
||||
id = member.userId,
|
||||
name = member.displayName,
|
||||
url = member.avatarUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.roomdetails.impl.members
|
||||
|
||||
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.RoomScope
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomMemberListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomMemberListPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private fun onUserSelected(matrixUser: MatrixUser) {
|
||||
Timber.d("TODO: implement user selection. User: $matrixUser")
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = { navigateUp() },
|
||||
onUserSelected = ::onUserSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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.features.roomdetails.impl.members
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class RoomMemberListPresenter @Inject constructor(
|
||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||
@Named("RoomMembers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||
) : Presenter<RoomMemberListState> {
|
||||
|
||||
private val userListPresenter by lazy {
|
||||
userListPresenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
matrixUserDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberListState {
|
||||
val userListState = userListPresenter.present()
|
||||
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
allUsers.value = Async.Success(matrixUserDataSource.search("").toImmutableList())
|
||||
}
|
||||
}
|
||||
return RoomMemberListState(
|
||||
allUsers = allUsers.value,
|
||||
userListState = userListState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.roomdetails.impl.members
|
||||
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomMemberListState(
|
||||
val allUsers: Async<ImmutableList<MatrixUser>>,
|
||||
val userListState: UserListState,
|
||||
// val eventSink: (AddPeopleEvents) -> Unit,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.roomdetails.impl.members
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.userlist.api.aUserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||
override val values: Sequence<RoomMemberListState>
|
||||
get() = sequenceOf(
|
||||
aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))),
|
||||
aRoomMemberListState(allUsers = Async.Loading())
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aRoomMemberListState(
|
||||
searchResults: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
||||
) =
|
||||
RoomMemberListState(
|
||||
userListState = aUserListState().copy(searchResults = searchResults),
|
||||
allUsers = allUsers,
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.roomdetails.impl.members
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.roomdetails.impl.R
|
||||
import io.element.android.features.userlist.api.SearchSingleUserResultItem
|
||||
import io.element.android.features.userlist.api.UserListView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
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.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomMemberListView(
|
||||
state: RoomMemberListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
onUserSelected: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
UserListView(
|
||||
state = state.userListState,
|
||||
onUserSelected = onUserSelected,
|
||||
)
|
||||
|
||||
if (!state.userListState.isSearchActive) {
|
||||
if (state.allUsers is Async.Success) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||
item {
|
||||
val memberCount = state.allUsers.state.count()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
text = pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount),
|
||||
style = ElementTextStyles.Regular.callout,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
items(state.allUsers.state) { matrixUser ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
matrixUser = matrixUser,
|
||||
onClick = { onUserSelected(matrixUser) }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (state.allUsers.isLoading()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RoomMemberListTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_room_details_people_title),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = onBackPressed) },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RoomMemberListState) {
|
||||
RoomMemberListView(state)
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"1 person"</item>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<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_invite_people_title">"Invite people"</string>
|
||||
|
||||
@@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
@@ -42,8 +43,25 @@ class RoomDetailsPresenterTests {
|
||||
Truth.assertThat(initialState.roomName).isEqualTo(room.name)
|
||||
Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
|
||||
Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic)
|
||||
Truth.assertThat(initialState.memberCount).isEqualTo(room.members.count())
|
||||
Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null))
|
||||
Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room member count is calculated asynchronously`() = runTest {
|
||||
val room = aMatrixRoom()
|
||||
val presenter = RoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.memberCount).isEqualTo(Async.Loading(null))
|
||||
|
||||
val finalState = awaitItem()
|
||||
Truth.assertThat(finalState.memberCount).isEqualTo(Async.Success(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +74,24 @@ class RoomDetailsPresenterTests {
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.roomName).isEqualTo(room.displayName)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can handle error while fetching member count`() = runTest {
|
||||
val room = aMatrixRoom(name = null).apply {
|
||||
givenFetchMemberResult(Result.failure(Throwable()))
|
||||
}
|
||||
val presenter = RoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
Truth.assertThat(awaitItem().memberCount).isInstanceOf(Async.Failure::class.java)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.roomdetails.members
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.internal.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class RoomMemberListPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - search is done automatically on start, but is async`() = runTest {
|
||||
val searchResult = listOf(aMatrixUser())
|
||||
val userListDataSource = FakeMatrixUserDataSource().apply {
|
||||
givenSearchResult(searchResult)
|
||||
}
|
||||
val userListFactory = object : UserListPresenter.Factory {
|
||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||
}
|
||||
val presenter = RoomMemberListPresenter(userListFactory, userListDataSource)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
Truth.assertThat(initialState.userListState.searchResults).isEmpty()
|
||||
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
||||
|
||||
val loadedState = awaitItem()
|
||||
Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.selectusers.api"
|
||||
namespace = "io.element.android.features.userlist.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
interface MatrixUserDataSource {
|
||||
suspend fun search(query: String): List<MatrixUser>
|
||||
suspend fun getProfile(userId: UserId): MatrixUser?
|
||||
}
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.api
|
||||
package io.element.android.features.userlist.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
|
||||
sealed interface UserListEvents {
|
||||
data class UpdateSearchQuery(val query: String) : UserListEvents
|
||||
data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents
|
||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : UserListEvents
|
||||
}
|
||||
@@ -14,13 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.api
|
||||
package io.element.android.features.userlist.api
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
interface SelectUsersPresenter : Presenter<SelectUsersState> {
|
||||
interface UserListPresenter : Presenter<UserListState> {
|
||||
|
||||
interface Factory {
|
||||
fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter
|
||||
fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.api
|
||||
package io.element.android.features.userlist.api
|
||||
|
||||
data class SelectUsersPresenterArgs(
|
||||
data class UserListPresenterArgs(
|
||||
val selectionMode: SelectionMode,
|
||||
)
|
||||
|
||||
@@ -14,20 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.api
|
||||
package io.element.android.features.userlist.api
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class SelectUsersState(
|
||||
data class UserListState(
|
||||
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 eventSink: (UserListEvents) -> Unit,
|
||||
) {
|
||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.api
|
||||
package io.element.android.features.userlist.api
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
@@ -22,25 +22,25 @@ 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>
|
||||
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||
override val values: Sequence<UserListState>
|
||||
get() = sequenceOf(
|
||||
aSelectUsersState(),
|
||||
aSelectUsersState().copy(
|
||||
aUserListState(),
|
||||
aUserListState().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(
|
||||
aUserListState().copy(isSearchActive = true),
|
||||
aUserListState().copy(isSearchActive = true, searchQuery = "someone"),
|
||||
aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple),
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
searchResults = aListOfResults(),
|
||||
),
|
||||
aSelectUsersState().copy(
|
||||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
@@ -50,7 +50,7 @@ open class SelectUsersStateProvider : PreviewParameterProvider<SelectUsersState>
|
||||
)
|
||||
}
|
||||
|
||||
fun aSelectUsersState() = SelectUsersState(
|
||||
fun aUserListState() = UserListState(
|
||||
isSearchActive = false,
|
||||
searchQuery = "",
|
||||
searchResults = persistentListOf(),
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.api
|
||||
package io.element.android.features.userlist.api
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -65,8 +65,8 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun SelectUsersView(
|
||||
state: SelectUsersState,
|
||||
fun UserListView(
|
||||
state: UserListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onUserSelected: (MatrixUser) -> Unit = {},
|
||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||
@@ -82,14 +82,14 @@ fun SelectUsersView(
|
||||
selectedUsersListState = state.selectedUsersListState,
|
||||
active = state.isSearchActive,
|
||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
||||
onActiveChanged = { state.eventSink(SelectUsersEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) },
|
||||
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
|
||||
onUserSelected = {
|
||||
state.eventSink(SelectUsersEvents.AddToSelection(it))
|
||||
state.eventSink(UserListEvents.AddToSelection(it))
|
||||
onUserSelected(it)
|
||||
},
|
||||
onUserDeselected = {
|
||||
state.eventSink(SelectUsersEvents.RemoveFromSelection(it))
|
||||
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||
onUserDeselected(it)
|
||||
},
|
||||
)
|
||||
@@ -100,7 +100,7 @@ fun SelectUsersView(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
selectedUsers = state.selectedUsers,
|
||||
onUserRemoved = {
|
||||
state.eventSink(SelectUsersEvents.RemoveFromSelection(it))
|
||||
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||
onUserDeselected(it)
|
||||
},
|
||||
)
|
||||
@@ -297,15 +297,15 @@ fun SelectedUser(
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectUsersViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) =
|
||||
internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectUsersViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) =
|
||||
internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: SelectUsersState) {
|
||||
SelectUsersView(state = state)
|
||||
private fun ContentToPreview(state: UserListState) {
|
||||
UserListView(state = state)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.selectusers.impl"
|
||||
namespace = "io.element.android.features.userlist.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
@@ -41,7 +41,7 @@ dependencies {
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.selectusers.api)
|
||||
api(projects.features.userlist.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
@@ -52,6 +52,7 @@ dependencies {
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.impl
|
||||
package io.element.android.features.userlist.impl
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -31,10 +31,11 @@ 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.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.features.userlist.api.UserListEvents
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
import io.element.android.features.userlist.api.UserListPresenter
|
||||
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
|
||||
@@ -45,18 +46,19 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DefaultSelectUsersPresenter @AssistedInject constructor(
|
||||
@Assisted val args: SelectUsersPresenterArgs,
|
||||
) : SelectUsersPresenter {
|
||||
class DefaultUserListPresenter @AssistedInject constructor(
|
||||
@Assisted val args: UserListPresenterArgs,
|
||||
@Assisted val matrixUserDataSource: MatrixUserDataSource,
|
||||
) : UserListPresenter {
|
||||
|
||||
@AssistedFactory
|
||||
@ContributesBinding(SessionScope::class)
|
||||
interface DefaultSelectUsersFactory : SelectUsersPresenter.Factory {
|
||||
override fun create(args: SelectUsersPresenterArgs): DefaultSelectUsersPresenter
|
||||
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||
override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): DefaultUserListPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): SelectUsersState {
|
||||
override fun present(): UserListState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedUsers: MutableState<ImmutableList<MatrixUser>> = remember {
|
||||
@@ -68,17 +70,17 @@ class DefaultSelectUsersPresenter @AssistedInject constructor(
|
||||
mutableStateOf(persistentListOf())
|
||||
}
|
||||
|
||||
fun handleEvents(event: SelectUsersEvents) {
|
||||
fun handleEvents(event: UserListEvents) {
|
||||
when (event) {
|
||||
is SelectUsersEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is SelectUsersEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is SelectUsersEvents.AddToSelection -> {
|
||||
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is UserListEvents.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()
|
||||
is UserListEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ class DefaultSelectUsersPresenter @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
return SelectUsersState(
|
||||
return UserListState(
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults.value,
|
||||
selectedUsers = selectedUsers.value.reversed().toImmutableList(),
|
||||
@@ -106,11 +108,11 @@ class DefaultSelectUsersPresenter @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun performSearch(query: String): ImmutableList<MatrixUser> {
|
||||
private suspend fun performSearch(query: String): ImmutableList<MatrixUser> {
|
||||
val isMatrixId = MatrixPatterns.isUserId(query)
|
||||
val results = mutableListOf<MatrixUser>()// TODO trigger /search request
|
||||
val results = matrixUserDataSource.search(query).toMutableList()
|
||||
if (isMatrixId && results.none { it.id.value == query }) {
|
||||
val getProfileResult: MatrixUser? = null // TODO trigger /profile request
|
||||
val getProfileResult: MatrixUser? = matrixUserDataSource.getProfile(UserId(query))
|
||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||
results.add(0, profile)
|
||||
}
|
||||
@@ -14,16 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.selectusers.impl
|
||||
package io.element.android.features.userlist.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.features.userlist.api.UserListEvents
|
||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
||||
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
|
||||
@@ -34,11 +35,16 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultSelectUsersPresenterTests {
|
||||
class DefaultUserListPresenterTests {
|
||||
|
||||
private val userListDataSource = FakeMatrixUserDataSource()
|
||||
|
||||
@Test
|
||||
fun `present - initial state for single selection`() = runTest {
|
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single))
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -53,7 +59,10 @@ class DefaultSelectUsersPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - initial state for multiple selection`() = runTest {
|
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Multiple))
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||
userListDataSource
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -68,26 +77,29 @@ class DefaultSelectUsersPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - update search query`() = runTest {
|
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single))
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(true))
|
||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
||||
assertThat(awaitItem().isSearchActive).isTrue()
|
||||
|
||||
val matrixIdQuery = "@name:matrix.org"
|
||||
initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(matrixIdQuery))
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery)))
|
||||
|
||||
val notMatrixIdQuery = "name"
|
||||
initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||
assertThat(awaitItem().searchResults).isEmpty()
|
||||
|
||||
initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(false))
|
||||
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||
assertThat(awaitItem().isSearchActive).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -97,7 +109,10 @@ class DefaultSelectUsersPresenterTests {
|
||||
mockkConstructor(LazyListState::class)
|
||||
coJustRun { anyConstructed<LazyListState>().scrollToItem(index = any()) }
|
||||
|
||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single))
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -108,23 +123,23 @@ class DefaultSelectUsersPresenterTests {
|
||||
val userABis = aMatrixUser("userA", "A")
|
||||
val userC = aMatrixUser("userC", "C")
|
||||
|
||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userA))
|
||||
initialState.eventSink(UserListEvents.AddToSelection(userA))
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userA)
|
||||
|
||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userB))
|
||||
initialState.eventSink(UserListEvents.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))
|
||||
initialState.eventSink(UserListEvents.AddToSelection(userABis))
|
||||
initialState.eventSink(UserListEvents.AddToSelection(userC))
|
||||
// duplicated users should be ignored
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA)
|
||||
|
||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userB))
|
||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userB))
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userA)
|
||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userA))
|
||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userA))
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC)
|
||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userC))
|
||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userC))
|
||||
assertThat(awaitItem().selectedUsers).isEmpty()
|
||||
}
|
||||
}
|
||||
32
features/userlist/test/build.gradle.kts
Normal file
32
features/userlist/test/build.gradle.kts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.userlist.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.userlist.api)
|
||||
api(libs.coroutines.core)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.test
|
||||
|
||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||
|
||||
class FakeMatrixUserDataSource : MatrixUserDataSource {
|
||||
|
||||
private var searchResult: List<MatrixUser> = emptyList()
|
||||
private var profile: MatrixUser? = null
|
||||
|
||||
override suspend fun search(query: String): List<MatrixUser> = searchResult
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? = profile
|
||||
|
||||
fun givenSearchResult(users: List<MatrixUser>) {
|
||||
this.searchResult = users
|
||||
}
|
||||
|
||||
fun givenUserProfile(matrixUser: MatrixUser?) {
|
||||
this.profile = matrixUser
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user