Show pending invitations in room members list (#385)
Splits a Room's member list in 2 showing pending invitees first and then the actual room member. This simple user facing change entails a host of under the hood changes: - It copies the logic from the `userlist` module and merges it into the `roomdetails` module removing all details not related to the member list (e.g. gets rid of multiple selection, debouncing etc.). - Uncouples the `roomdetails` module from the `userlist` one. Now leaving only the `createroom` module to depend on the `userlist` module. Therefore the `userlist` module could be in the future completely removed and merged into the `createroom` module. - Changes the room members count in the room details screen to only show the members who have joined (i.e. don't count those still in the invited state). Missed ACs: - This change does not make the member list live update. Discussion is ongoing on how to make this technically feasible. Parent issue: - https://github.com/vector-im/element-x-android/issues/246
This commit is contained in:
parent
02e0216f83
commit
b51c19af19
26 changed files with 477 additions and 184 deletions
1
changelog.d/385.feature
Normal file
1
changelog.d/385.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Show pending invitations in room members list
|
||||||
|
|
@ -40,7 +40,6 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.features.userlist.api)
|
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
api(projects.features.roomdetails.api)
|
api(projects.features.roomdetails.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
@ -51,7 +50,6 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.impl)
|
|
||||||
testImplementation(projects.features.userlist.test)
|
testImplementation(projects.features.userlist.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -124,7 +125,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
||||||
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
||||||
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
||||||
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size)
|
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,31 +17,17 @@
|
||||||
package io.element.android.features.roomdetails.impl.di
|
package io.element.android.features.roomdetails.impl.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource
|
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(RoomScope::class)
|
@ContributesTo(RoomScope::class)
|
||||||
interface RoomMemberBindsModule {
|
object RoomMemberModule {
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Named("RoomMembers")
|
|
||||||
fun bindRoomMemberUserListDataSource(dataSource: RoomUserListDataSource): UserListDataSource
|
|
||||||
}
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@ContributesTo(RoomScope::class)
|
|
||||||
object RoomMemberProvidesModule {
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRoomMemberDetailsPresenterFactory(
|
fun provideRoomMemberDetailsPresenterFactory(
|
||||||
|
|
@ -16,27 +16,23 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
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.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import kotlinx.coroutines.flow.dropWhile
|
import kotlinx.coroutines.flow.dropWhile
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomUserListDataSource @Inject constructor(
|
class RoomMemberListDataSource @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : UserListDataSource {
|
) {
|
||||||
|
|
||||||
override suspend fun search(query: String): List<MatrixUser> = withContext(coroutineDispatchers.io) {
|
suspend fun search(query: String): List<RoomMember> = withContext(coroutineDispatchers.io) {
|
||||||
val roomMembers = room.membersStateFlow
|
val roomMembers = room.membersStateFlow
|
||||||
.dropWhile { it !is MatrixRoomMembersState.Ready }
|
.dropWhile { it !is MatrixRoomMembersState.Ready }
|
||||||
.first()
|
.first()
|
||||||
|
|
@ -50,11 +46,7 @@ class RoomUserListDataSource @Inject constructor(
|
||||||
|| member.displayName?.contains(query, ignoreCase = true).orFalse()
|
|| member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filteredMembers.map(RoomMember::toMatrixUser)
|
filteredMembers
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
|
|
||||||
sealed interface RoomMemberListEvents {
|
sealed interface RoomMemberListEvents {
|
||||||
data class SelectUser(val user: MatrixUser) : RoomMemberListEvents
|
data class UpdateSearchQuery(val query: String) : RoomMemberListEvents
|
||||||
|
data class OnSearchActiveChanged(val active: Boolean) : RoomMemberListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,54 +18,73 @@ package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
class RoomMemberListPresenter @Inject constructor(
|
class RoomMemberListPresenter @Inject constructor(
|
||||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||||
@Named("RoomMembers") private val userListDataSource: UserListDataSource,
|
|
||||||
private val userListDataStore: UserListDataStore,
|
|
||||||
private val room: MatrixRoom,
|
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : Presenter<RoomMemberListState> {
|
) : Presenter<RoomMemberListState> {
|
||||||
|
|
||||||
private val userListPresenter by lazy {
|
|
||||||
userListPresenterFactory.create(
|
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
|
||||||
userListDataSource,
|
|
||||||
userListDataStore,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMemberListState {
|
override fun present(): RoomMemberListState {
|
||||||
val userListState = userListPresenter.present()
|
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||||
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
var searchResults by remember {
|
||||||
|
mutableStateOf<RoomMemberSearchResultState>(RoomMemberSearchResultState.NotSearching)
|
||||||
|
}
|
||||||
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
allUsers.value = Async.Success(userListDataSource.search("").toImmutableList())
|
val members = roomMemberListDataSource.search("").groupBy { it.membership }
|
||||||
|
roomMembers = Async.Success(
|
||||||
|
RoomMembers(
|
||||||
|
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
|
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(searchQuery) {
|
||||||
|
withContext(coroutineDispatchers.io) {
|
||||||
|
searchResults = if (searchQuery.isEmpty()) {
|
||||||
|
RoomMemberSearchResultState.NotSearching
|
||||||
|
} else {
|
||||||
|
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
||||||
|
if (results.isEmpty()) RoomMemberSearchResultState.NoResults
|
||||||
|
else RoomMemberSearchResultState.Results(
|
||||||
|
RoomMembers(
|
||||||
|
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
|
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomMemberListState(
|
return RoomMemberListState(
|
||||||
allUsers = allUsers.value,
|
roomMembers = roomMembers,
|
||||||
userListState = userListState,
|
searchQuery = searchQuery,
|
||||||
|
searchResults = searchResults,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
eventSink = { event ->
|
||||||
|
when (event) {
|
||||||
|
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||||
|
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,30 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
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.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomMemberListState(
|
data class RoomMemberListState(
|
||||||
val allUsers: Async<ImmutableList<MatrixUser>>,
|
val roomMembers: Async<RoomMembers>,
|
||||||
val userListState: UserListState,
|
val searchQuery: String,
|
||||||
|
val searchResults: RoomMemberSearchResultState,
|
||||||
|
val isSearchActive: Boolean,
|
||||||
|
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RoomMembers(
|
||||||
|
val invited: ImmutableList<RoomMember>,
|
||||||
|
val joined: ImmutableList<RoomMember>
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface RoomMemberSearchResultState {
|
||||||
|
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
||||||
|
object NotSearching : RoomMemberSearchResultState
|
||||||
|
|
||||||
|
/** The search has completed, but no results were found. */
|
||||||
|
object NoResults : RoomMemberSearchResultState
|
||||||
|
|
||||||
|
/** The search has completed, and some matching users were found. */
|
||||||
|
data class Results(val results: RoomMembers) : RoomMemberSearchResultState
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,93 @@
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||||
override val values: Sequence<RoomMemberListState>
|
override val values: Sequence<RoomMemberListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))),
|
aRoomMemberListState(
|
||||||
aRoomMemberListState(allUsers = Async.Loading())
|
roomMembers = Async.Success(
|
||||||
|
RoomMembers(
|
||||||
|
invited = persistentListOf(aVictor(), aWalter()),
|
||||||
|
joined = persistentListOf(anAlice(), aBob()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aRoomMemberListState(roomMembers = Async.Loading()),
|
||||||
|
aRoomMemberListState().copy(isSearchActive = false),
|
||||||
|
aRoomMemberListState().copy(isSearchActive = true),
|
||||||
|
aRoomMemberListState().copy(isSearchActive = true, searchQuery = "someone"),
|
||||||
|
aRoomMemberListState().copy(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchQuery = "@someone:matrix.org",
|
||||||
|
searchResults = RoomMemberSearchResultState.Results(
|
||||||
|
RoomMembers(
|
||||||
|
invited = persistentListOf(aVictor()),
|
||||||
|
joined = persistentListOf(anAlice()),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aRoomMemberListState().copy(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchQuery = "something-with-no-results",
|
||||||
|
searchResults = RoomMemberSearchResultState.NoResults
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomMemberListState(
|
internal fun aRoomMemberListState(
|
||||||
searchResults: UserSearchResultState = UserSearchResultState.NotSearching,
|
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
searchResults: RoomMemberSearchResultState = RoomMemberSearchResultState.NotSearching,
|
||||||
) =
|
) = RoomMemberListState(
|
||||||
RoomMemberListState(
|
roomMembers = roomMembers,
|
||||||
userListState = aUserListState().copy(searchResults = searchResults),
|
searchQuery = "",
|
||||||
allUsers = allUsers,
|
searchResults = searchResults,
|
||||||
)
|
isSearchActive = false,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aRoomMember(
|
||||||
|
userId: UserId = UserId("@alice:server.org"),
|
||||||
|
displayName: String? = null,
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||||
|
isNameAmbiguous: Boolean = false,
|
||||||
|
powerLevel: Long = 0L,
|
||||||
|
normalizedPowerLevel: Long = 0L,
|
||||||
|
isIgnored: Boolean = false,
|
||||||
|
) = RoomMember(
|
||||||
|
userId = userId,
|
||||||
|
displayName = displayName,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
membership = membership,
|
||||||
|
isNameAmbiguous = isNameAmbiguous,
|
||||||
|
powerLevel = powerLevel,
|
||||||
|
normalizedPowerLevel = normalizedPowerLevel,
|
||||||
|
isIgnored = isIgnored,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aRoomMemberList() = listOf(
|
||||||
|
anAlice(),
|
||||||
|
aBob(),
|
||||||
|
aRoomMember(UserId("@carol:server.org"), "Carol"),
|
||||||
|
aRoomMember(UserId("@david:server.org"), "David"),
|
||||||
|
aRoomMember(UserId("@eve:server.org"), "Eve"),
|
||||||
|
aRoomMember(UserId("@justin:server.org"), "Justin"),
|
||||||
|
aRoomMember(UserId("@mallory:server.org"), "Mallory"),
|
||||||
|
aRoomMember(UserId("@susie:server.org"), "Susie"),
|
||||||
|
aVictor(),
|
||||||
|
aWalter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice")
|
||||||
|
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob")
|
||||||
|
|
||||||
|
fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE)
|
||||||
|
|
||||||
|
fun aWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE)
|
||||||
|
|
|
||||||
|
|
@ -16,20 +16,31 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -39,37 +50,43 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.features.userlist.api.components.SearchSingleUserResultItem
|
|
||||||
import io.element.android.features.userlist.api.components.UserListView
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.isLoading
|
import io.element.android.libraries.architecture.isLoading
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListView(
|
fun RoomMemberListView(
|
||||||
state: RoomMemberListState,
|
state: RoomMemberListState,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onMemberSelected: (UserId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
|
||||||
onMemberSelected: (UserId) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onUserSelected(user: MatrixUser) {
|
fun onUserSelected(roomMember: RoomMember) {
|
||||||
onMemberSelected(user.userId)
|
onMemberSelected(roomMember.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.userListState.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,33 +97,26 @@ fun RoomMemberListView(
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
UserListView(
|
Column {
|
||||||
state = state.userListState,
|
RoomMemberSearchBar(
|
||||||
onUserSelected = ::onUserSelected,
|
query = state.searchQuery,
|
||||||
)
|
state = state.searchResults,
|
||||||
|
active = state.isSearchActive,
|
||||||
|
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
||||||
|
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
||||||
|
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
||||||
|
onUserSelected = ::onUserSelected,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.userListState.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
if (state.allUsers is Async.Success) {
|
if (state.roomMembers is Async.Success) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
RoomMemberList(
|
||||||
item {
|
roomMembers = state.roomMembers.state,
|
||||||
val memberCount = state.allUsers.state.count()
|
onUserSelected = ::onUserSelected,
|
||||||
Text(
|
)
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
} else if (state.roomMembers.isLoading()) {
|
||||||
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) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
|
|
@ -116,9 +126,73 @@ fun RoomMemberListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomMemberList(
|
||||||
|
roomMembers: RoomMembers,
|
||||||
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||||
|
if (roomMembers.invited.isNotEmpty()) {
|
||||||
|
roomMemberListSection(
|
||||||
|
headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) },
|
||||||
|
members = roomMembers.invited,
|
||||||
|
onMemberSelected = { onUserSelected(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (roomMembers.joined.isNotEmpty()) {
|
||||||
|
val memberCount = roomMembers.joined.count()
|
||||||
|
roomMemberListSection(
|
||||||
|
headerText = { pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) },
|
||||||
|
members = roomMembers.joined,
|
||||||
|
onMemberSelected = { onUserSelected(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.roomMemberListSection(
|
||||||
|
headerText: @Composable () -> String,
|
||||||
|
members: ImmutableList<RoomMember>,
|
||||||
|
onMemberSelected: (RoomMember) -> Unit,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
text = headerText(),
|
||||||
|
style = ElementTextStyles.Regular.callout,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(members) { matrixUser ->
|
||||||
|
RoomMemberListItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
roomMember = matrixUser,
|
||||||
|
onClick = { onMemberSelected(matrixUser) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomMemberListItem(
|
||||||
|
roomMember: RoomMember,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
|
matrixUser = MatrixUser(
|
||||||
|
userId = roomMember.userId,
|
||||||
|
displayName = roomMember.displayName,
|
||||||
|
avatarUrl = roomMember.avatarUrl
|
||||||
|
),
|
||||||
|
avatarSize = AvatarSize.Custom(36.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListTopBar(
|
private fun RoomMemberListTopBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -135,6 +209,86 @@ fun RoomMemberListTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RoomMemberSearchBar(
|
||||||
|
query: String,
|
||||||
|
state: RoomMemberSearchResultState,
|
||||||
|
active: Boolean,
|
||||||
|
placeHolderTitle: String,
|
||||||
|
onActiveChanged: (Boolean) -> Unit,
|
||||||
|
onTextChanged: (String) -> Unit,
|
||||||
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
onTextChanged("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onTextChanged,
|
||||||
|
onSearch = { focusManager.clearFocus() },
|
||||||
|
active = active,
|
||||||
|
onActiveChange = onActiveChanged,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = placeHolderTitle,
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = if (active) {
|
||||||
|
{ BackButton(onClick = { onActiveChanged(false) }) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
trailingIcon = when {
|
||||||
|
active && query.isNotEmpty() -> {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onTextChanged("") }) {
|
||||||
|
Icon(Icons.Default.Close, stringResource(StringR.string.action_clear))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!active -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = stringResource(StringR.string.action_search),
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
content = {
|
||||||
|
if (state is RoomMemberSearchResultState.Results) {
|
||||||
|
RoomMemberList(
|
||||||
|
roomMembers = state.results,
|
||||||
|
onUserSelected = { onUserSelected(it) }
|
||||||
|
)
|
||||||
|
} else if (state is RoomMemberSearchResultState.NoResults) {
|
||||||
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(StringR.string.common_no_results),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||||
|
|
@ -147,5 +301,9 @@ fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::cla
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: RoomMemberListState) {
|
private fun ContentToPreview(state: RoomMemberListState) {
|
||||||
RoomMemberListView(state)
|
RoomMemberListView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = {},
|
||||||
|
onMemberSelected = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import io.element.android.features.roomdetails.impl.LeaveRoomWarning
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -90,7 +92,7 @@ class RoomDetailsPresenterTests {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val roomMembers = listOf(
|
val roomMembers = listOf(
|
||||||
aRoomMember(A_USER_ID),
|
aRoomMember(A_USER_ID),
|
||||||
aRoomMember(A_USER_ID_2),
|
aRoomMember(A_USER_ID_2, membership = RoomMembershipState.INVITE),
|
||||||
)
|
)
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -112,7 +114,7 @@ class RoomDetailsPresenterTests {
|
||||||
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||||
//skipItems(1)
|
//skipItems(1)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(roomMembers.size))
|
Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(1))
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
|
|
@ -266,22 +268,3 @@ fun aMatrixRoom(
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMember(
|
|
||||||
userId: UserId = A_USER_ID,
|
|
||||||
displayName: String? = null,
|
|
||||||
avatarUrl: String? = null,
|
|
||||||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous: Boolean = false,
|
|
||||||
powerLevel: Long = 0L,
|
|
||||||
normalizedPowerLevel: Long = 0L,
|
|
||||||
isIgnored: Boolean = false,
|
|
||||||
) = RoomMember(
|
|
||||||
userId = userId,
|
|
||||||
displayName = displayName,
|
|
||||||
avatarUrl = avatarUrl,
|
|
||||||
membership = membership,
|
|
||||||
isNameAmbiguous = isNameAmbiguous,
|
|
||||||
powerLevel = powerLevel,
|
|
||||||
normalizedPowerLevel = normalizedPowerLevel,
|
|
||||||
isIgnored = isIgnored,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -20,62 +20,111 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||||
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.roomdetails.impl.members.RoomMemberSearchResultState
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.internal.toImmutableList
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class RoomMemberListPresenterTests {
|
class RoomMemberListPresenterTests {
|
||||||
|
|
||||||
private val testCoroutineDispatchers = testCoroutineDispatchers()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - search is done automatically on start, but is async`() = runTest {
|
fun `search is done automatically on start, but is async`() = runTest {
|
||||||
val searchResult = listOf(aMatrixUser())
|
val presenter = createPresenter()
|
||||||
val userListDataSource = FakeUserListDataSource().apply {
|
|
||||||
givenSearchResult(searchResult)
|
|
||||||
}
|
|
||||||
val userListDataStore = UserListDataStore()
|
|
||||||
val userListFactory = object : UserListPresenter.Factory {
|
|
||||||
override fun create(
|
|
||||||
args: UserListPresenterArgs,
|
|
||||||
userListDataSource: UserListDataSource,
|
|
||||||
userListDataStore: UserListDataStore,
|
|
||||||
) = DefaultUserListPresenter(args, userListDataSource, userListDataStore)
|
|
||||||
}
|
|
||||||
val fakeRoom = FakeMatrixRoom()
|
|
||||||
val presenter = RoomMemberListPresenter(
|
|
||||||
userListPresenterFactory = userListFactory,
|
|
||||||
userListDataSource = userListDataSource,
|
|
||||||
userListDataStore = userListDataStore,
|
|
||||||
room = fakeRoom,
|
|
||||||
coroutineDispatchers = testCoroutineDispatchers
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
||||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
Truth.assertThat(initialState.searchQuery).isEmpty()
|
||||||
Truth.assertThat(initialState.userListState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
Truth.assertThat(initialState.searchResults).isEqualTo(RoomMemberSearchResultState.NotSearching)
|
||||||
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
Truth.assertThat(initialState.isSearchActive).isFalse()
|
||||||
|
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList())
|
Truth.assertThat(loadedState.roomMembers).isInstanceOf(Async.Success::class.java)
|
||||||
|
Truth.assertThat((loadedState.roomMembers as Async.Success).state.invited).isEqualTo(listOf(aVictor(), aWalter()))
|
||||||
|
Truth.assertThat((loadedState.roomMembers as Async.Success).state.joined).isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open search`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
|
||||||
|
val searchActiveState = awaitItem()
|
||||||
|
Truth.assertThat((searchActiveState.isSearchActive)).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search for something which is not found`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
val searchActiveState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something"))
|
||||||
|
val searchQueryUpdatedState = awaitItem()
|
||||||
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
||||||
|
val searchSearchResultDelivered = awaitItem()
|
||||||
|
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.NoResults::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search for something which is found`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
val searchActiveState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice"))
|
||||||
|
val searchQueryUpdatedState = awaitItem()
|
||||||
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
||||||
|
val searchSearchResultDelivered = awaitItem()
|
||||||
|
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.Results::class.java)
|
||||||
|
Truth.assertThat((searchSearchResultDelivered.searchResults as RoomMemberSearchResultState.Results).results.joined.first().displayName)
|
||||||
|
.isEqualTo("Alice")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
private fun createDataSource(
|
||||||
|
matrixRoom: MatrixRoom = aMatrixRoom().apply {
|
||||||
|
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||||
|
},
|
||||||
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers)
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
private fun createPresenter(
|
||||||
|
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(),
|
||||||
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
) = RoomMemberListPresenter(roomMemberListDataSource, coroutineDispatchers)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomdetails.aMatrixClient
|
import io.element.android.features.roomdetails.aMatrixClient
|
||||||
import io.element.android.features.roomdetails.aMatrixRoom
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.libraries.matrix.api.room
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
|
|
||||||
data class RoomMember(
|
data class RoomMember(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
|
|
@ -30,12 +29,6 @@ data class RoomMember(
|
||||||
val isIgnored: Boolean,
|
val isIgnored: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun RoomMember.toMatrixUser() = MatrixUser(
|
|
||||||
userId = userId,
|
|
||||||
displayName = displayName,
|
|
||||||
avatarUrl = avatarUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class RoomMembershipState {
|
enum class RoomMembershipState {
|
||||||
BAN, INVITE, JOIN, KNOCK, LEAVE
|
BAN, INVITE, JOIN, KNOCK, LEAVE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6893108aabbf06f43af90f17b6b1a8a521312559f407647ce08db06ecb9a8f84
|
oid sha256:29fa45284bef52648e02629a440b335595c7d4a4d04d0da5c4acbe9ae457bad6
|
||||||
size 22033
|
size 46918
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:49e7533e6afba903219942193e0cc5cfbe2f67ba3b57516f77c259e7c42e8e3f
|
||||||
|
size 11813
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:89ea65099fb4981bbeb24e49afb7400b0e8da79e3b783e198519ba1e970404a8
|
||||||
|
size 8317
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9a8ba207cf61c56b64c6855d04c8a30def0b3daf325adf57edd45b40981ed745
|
||||||
|
size 7733
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:92c4537ed8794f4db08b4edec74a0de41847c4fd8c4fe03b7112bc63124b0a61
|
||||||
|
size 29326
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d5b879b74654fdad0638f432a71adfc9186fbe00dafd5d963a3affb33ec8c5c8
|
||||||
|
size 12841
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3e7b5bd916d4d3067b5013400b4ac864fab560e96fcb75ec895684021a00b8ba
|
oid sha256:3d40819700e3cbe9ca8ae1e5886de036a6dc05a7da6dd366490cc79c27ae3e8d
|
||||||
size 21808
|
size 45653
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9c3abebbe9e55706af5f4d1089e4308b0e975258a9d6a0e1793a4208b36c9c93
|
||||||
|
size 11754
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b8de2f28cf9918a7eddcc1311c34ba0376242a4708724b57689df000b7480524
|
||||||
|
size 8197
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:687133e4729ea42382ac0b76af6ceaf8b20cbc65923412fb97b077d2475c70f7
|
||||||
|
size 7514
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1f663a70c43b9f2e66b651f17169b63859fe7c87c21312b61ed61a9136eabe5f
|
||||||
|
size 27989
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:534cba0c13aa52b3557ab8df854c521dd13b82e5a1550d73abcef12925e389da
|
||||||
|
size 11878
|
||||||
Loading…
Add table
Add a link
Reference in a new issue