[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:
parent
f0b95d30be
commit
d7a6779343
62 changed files with 1159 additions and 157 deletions
|
|
@ -69,6 +69,8 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
||||||
|
private val roomFlowPresenter = RoomFlowPresenter(inputs.room)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
|
|
@ -110,6 +112,7 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
roomFlowPresenter.present()
|
||||||
Children(
|
Children(
|
||||||
navModel = backstack,
|
navModel = backstack,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appnav
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class RoomFlowPresenter(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
) : Presenter<RoomFlowState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): RoomFlowState {
|
||||||
|
// Preload room members so we can quickly detect if the room is a DM room
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
room.fetchMembers()
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.v("Success fetching members for room ${room.roomId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoomFlowState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At first the return type was Unit, but detekt complained about it
|
||||||
|
object RoomFlowState
|
||||||
|
|
@ -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.appnav
|
||||||
|
|
||||||
|
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.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
|
class RoomFlowPresenterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - fetches room members`() = runTest {
|
||||||
|
val fakeTimeline = FakeMatrixTimeline()
|
||||||
|
val room = FakeMatrixRoom(matrixTimeline = fakeTimeline)
|
||||||
|
val presenter = RoomFlowPresenter(room)
|
||||||
|
|
||||||
|
Truth.assertThat(room.areMembersFetched).isFalse()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
Truth.assertThat(room.areMembersFetched).isTrue()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - recovers from error while fetching room members`() = runTest {
|
||||||
|
val fakeTimeline = FakeMatrixTimeline()
|
||||||
|
val room = FakeMatrixRoom(matrixTimeline = fakeTimeline).apply {
|
||||||
|
givenFetchMemberResult(Result.failure(IllegalStateException("Some error")))
|
||||||
|
}
|
||||||
|
val presenter = RoomFlowPresenter(room)
|
||||||
|
|
||||||
|
Truth.assertThat(room.areMembersFetched).isFalse()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
Truth.assertThat(room.areMembersFetched).isFalse()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
Implement Room Details screen
|
Implement Room Details screen
|
||||||
|
Implement Room Member List screen
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ 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.selectusers.api)
|
implementation(projects.features.userlist.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -56,7 +56,8 @@ 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.selectusers.impl)
|
testImplementation(projects.features.userlist.impl)
|
||||||
|
testImplementation(projects.features.userlist.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
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
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenter
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||||
import io.element.android.features.selectusers.api.SelectionMode
|
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.architecture.Presenter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
class AddPeoplePresenter @Inject constructor(
|
class AddPeoplePresenter @Inject constructor(
|
||||||
private val selectUsersPresenterFactory: SelectUsersPresenter.Factory,
|
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||||
|
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||||
) : Presenter<AddPeopleState> {
|
) : Presenter<AddPeopleState> {
|
||||||
|
|
||||||
private val selectUsersPresenter by lazy {
|
private val userListPresenter by lazy {
|
||||||
selectUsersPresenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Multiple))
|
userListPresenterFactory.create(
|
||||||
|
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||||
|
matrixUserDataSource,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AddPeopleState {
|
override fun present(): AddPeopleState {
|
||||||
val selectUsersState = selectUsersPresenter.present()
|
val userListState = userListPresenter.present()
|
||||||
|
|
||||||
fun handleEvents(event: AddPeopleEvents) {
|
fun handleEvents(event: AddPeopleEvents) {
|
||||||
// do nothing for now
|
// do nothing for now
|
||||||
}
|
}
|
||||||
|
|
||||||
return AddPeopleState(
|
return AddPeopleState(
|
||||||
selectUsersState = selectUsersState,
|
userListState = userListState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
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(
|
data class AddPeopleState(
|
||||||
val selectUsersState: SelectUsersState,
|
val userListState: UserListState,
|
||||||
val eventSink: (AddPeopleEvents) -> Unit,
|
val eventSink: (AddPeopleEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,22 @@
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.selectusers.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.selectusers.api.aListOfSelectedUsers
|
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||||
import io.element.android.features.selectusers.api.aSelectUsersState
|
import io.element.android.features.userlist.api.aUserListState
|
||||||
|
|
||||||
open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
||||||
override val values: Sequence<AddPeopleState>
|
override val values: Sequence<AddPeopleState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aAddPeopleState(),
|
aAddPeopleState(),
|
||||||
aAddPeopleState().copy(
|
aAddPeopleState().copy(
|
||||||
selectUsersState = aSelectUsersState().copy(
|
userListState = aUserListState().copy(
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
aAddPeopleState().copy(
|
aAddPeopleState().copy(
|
||||||
selectUsersState = aSelectUsersState().copy(
|
userListState = aUserListState().copy(
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
|
|
@ -42,6 +42,6 @@ open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aAddPeopleState() = AddPeopleState(
|
fun aAddPeopleState() = AddPeopleState(
|
||||||
selectUsersState = aSelectUsersState(),
|
userListState = aUserListState(),
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import io.element.android.features.userlist.api.UserListView
|
||||||
import io.element.android.features.createroom.impl.R
|
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.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
|
||||||
|
|
@ -52,9 +52,9 @@ fun AddPeopleView(
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.selectUsersState.isSearchActive) {
|
if (!state.userListState.isSearchActive) {
|
||||||
AddPeopleViewTopBar(
|
AddPeopleViewTopBar(
|
||||||
hasSelectedUsers = state.selectUsersState.selectedUsers.isNotEmpty(),
|
hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(),
|
||||||
onBackPressed = onBackPressed,
|
onBackPressed = onBackPressed,
|
||||||
onNextPressed = onNextPressed,
|
onNextPressed = onNextPressed,
|
||||||
)
|
)
|
||||||
|
|
@ -66,9 +66,9 @@ fun AddPeopleView(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
SelectUsersView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenter
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||||
import io.element.android.features.selectusers.api.SelectionMode
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
class CreateRoomRootPresenter @Inject constructor(
|
class CreateRoomRootPresenter @Inject constructor(
|
||||||
private val presenterFactory: SelectUsersPresenter.Factory,
|
private val presenterFactory: UserListPresenter.Factory,
|
||||||
|
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
||||||
) : Presenter<CreateRoomRootState> {
|
) : Presenter<CreateRoomRootState> {
|
||||||
|
|
||||||
private val presenter by lazy {
|
private val presenter by lazy {
|
||||||
presenterFactory.create(SelectUsersPresenterArgs(SelectionMode.Single))
|
presenterFactory.create(
|
||||||
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
|
matrixUserDataSource,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): CreateRoomRootState {
|
override fun present(): CreateRoomRootState {
|
||||||
val selectUsersState = presenter.present()
|
val userListState = presenter.present()
|
||||||
|
|
||||||
fun handleEvents(event: CreateRoomRootEvents) {
|
fun handleEvents(event: CreateRoomRootEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -45,7 +51,7 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreateRoomRootState(
|
return CreateRoomRootState(
|
||||||
selectUsersState = selectUsersState,
|
userListState = userListState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
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(
|
data class CreateRoomRootState(
|
||||||
val selectUsersState: SelectUsersState,
|
val userListState: UserListState,
|
||||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.selectusers.api.aSelectUsersState
|
import io.element.android.features.userlist.api.aUserListState
|
||||||
|
|
||||||
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
||||||
override val values: Sequence<CreateRoomRootState>
|
override val values: Sequence<CreateRoomRootState>
|
||||||
|
|
@ -28,5 +28,5 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
||||||
|
|
||||||
fun aCreateRoomRootState() = CreateRoomRootState(
|
fun aCreateRoomRootState() = CreateRoomRootState(
|
||||||
eventSink = {},
|
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.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.userlist.api.UserListView
|
||||||
import io.element.android.features.createroom.impl.R
|
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.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
|
||||||
|
|
@ -60,7 +60,7 @@ fun CreateRoomRootView(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.selectUsersState.isSearchActive) {
|
if (!state.userListState.isSearchActive) {
|
||||||
CreateRoomRootViewTopBar(onClosePressed = onClosePressed)
|
CreateRoomRootViewTopBar(onClosePressed = onClosePressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,13 +69,13 @@ fun CreateRoomRootView(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
SelectUsersView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = state.selectUsersState,
|
state = state.userListState,
|
||||||
onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) },
|
onUserSelected = { state.eventSink.invoke(CreateRoomRootEvents.StartDM(it)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!state.selectUsersState.isSearchActive) {
|
if (!state.userListState.isSearchActive) {
|
||||||
CreateRoomActionButtonsList(
|
CreateRoomActionButtonsList(
|
||||||
onNewRoomClicked = onNewRoomClicked,
|
onNewRoomClicked = onNewRoomClicked,
|
||||||
onInvitePeopleClicked = { state.eventSink(CreateRoomRootEvents.InvitePeople) },
|
onInvitePeopleClicked = { state.eventSink(CreateRoomRootEvents.InvitePeople) },
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
|
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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
@ -35,10 +37,11 @@ class AddPeoplePresenterTests {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val selectUsersFactory = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
val userListFactory = object : DefaultUserListPresenter.DefaultUserListFactory {
|
||||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||||
}
|
}
|
||||||
presenter = AddPeoplePresenter(selectUsersFactory)
|
val dataSource = FakeMatrixUserDataSource()
|
||||||
|
presenter = AddPeoplePresenter(userListFactory, dataSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -51,3 +54,4 @@ class AddPeoplePresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||||
import io.element.android.features.selectusers.impl.DefaultSelectUsersPresenter
|
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.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -33,14 +35,16 @@ import org.junit.Test
|
||||||
|
|
||||||
class CreateRoomRootPresenterTests {
|
class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
|
private lateinit var userListDataSource: FakeMatrixUserDataSource
|
||||||
private lateinit var presenter: CreateRoomRootPresenter
|
private lateinit var presenter: CreateRoomRootPresenter
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val selectUsersPresenter = object : DefaultSelectUsersPresenter.DefaultSelectUsersFactory {
|
val userListPresenter = object : DefaultUserListPresenter.DefaultUserListFactory {
|
||||||
override fun create(args: SelectUsersPresenterArgs) = DefaultSelectUsersPresenter(args)
|
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
||||||
}
|
}
|
||||||
presenter = CreateRoomRootPresenter(selectUsersPresenter)
|
userListDataSource = FakeMatrixUserDataSource()
|
||||||
|
presenter = CreateRoomRootPresenter(userListPresenter, userListDataSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ 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)
|
||||||
|
|
@ -52,6 +53,8 @@ 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)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
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.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
|
@ -49,11 +51,25 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RoomDetails : NavTarget
|
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 {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
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.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
|
@ -40,6 +41,12 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
private val callback = plugins<RoomDetailsFlowNode.Callback>().firstOrNull()
|
||||||
|
|
||||||
|
private fun openRoomMemberList() {
|
||||||
|
callback?.openRoomMemberList()
|
||||||
|
}
|
||||||
|
|
||||||
private fun onShareRoom(context: Context) {
|
private fun onShareRoom(context: Context) {
|
||||||
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
||||||
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
|
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
|
||||||
|
|
@ -64,6 +71,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
goBack = { navigateUp() },
|
goBack = { navigateUp() },
|
||||||
onShareRoom = { onShareRoom(context) },
|
onShareRoom = { onShareRoom(context) },
|
||||||
|
openRoomMemberList = ::openRoomMemberList,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,16 @@
|
||||||
package io.element.android.features.roomdetails.impl
|
package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomDetailsPresenter @Inject constructor(
|
class RoomDetailsPresenter @Inject constructor(
|
||||||
|
|
@ -29,13 +37,24 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
override fun present(): RoomDetailsState {
|
override fun present(): RoomDetailsState {
|
||||||
// fun handleEvents(event: RoomDetailsEvent) {}
|
// 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(
|
return RoomDetailsState(
|
||||||
roomId = room.roomId.value,
|
roomId = room.roomId.value,
|
||||||
roomName = room.name ?: room.displayName,
|
roomName = room.name ?: room.displayName,
|
||||||
roomAlias = room.alias,
|
roomAlias = room.alias,
|
||||||
roomAvatarUrl = room.avatarUrl,
|
roomAvatarUrl = room.avatarUrl,
|
||||||
roomTopic = room.topic,
|
roomTopic = room.topic,
|
||||||
memberCount = room.members.size,
|
memberCount = memberCount,
|
||||||
isEncrypted = room.isEncrypted,
|
isEncrypted = room.isEncrypted,
|
||||||
// eventSink = ::handleEvents
|
// eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,15 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl
|
package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
data class RoomDetailsState(
|
data class RoomDetailsState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
val roomAlias: String?,
|
val roomAlias: String?,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
val roomTopic: String?,
|
val roomTopic: String?,
|
||||||
val memberCount: Int,
|
val memberCount: Async<Int>,
|
||||||
val isEncrypted: Boolean,
|
val isEncrypted: Boolean,
|
||||||
// val eventSink: (RoomDetailsEvent) -> Unit
|
// val eventSink: (RoomDetailsEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.roomdetails.impl
|
package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||||
override val values: Sequence<RoomDetailsState>
|
override val values: Sequence<RoomDetailsState>
|
||||||
|
|
@ -25,6 +26,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||||
aRoomDetailsState().copy(roomTopic = null),
|
aRoomDetailsState().copy(roomTopic = null),
|
||||||
aRoomDetailsState().copy(isEncrypted = false),
|
aRoomDetailsState().copy(isEncrypted = false),
|
||||||
aRoomDetailsState().copy(roomAlias = null),
|
aRoomDetailsState().copy(roomAlias = null),
|
||||||
|
aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +41,7 @@ fun aRoomDetailsState() = RoomDetailsState(
|
||||||
"|| MAIL iki/Marketing " +
|
"|| MAIL iki/Marketing " +
|
||||||
"|| MAI iki/Marketing " +
|
"|| MAI iki/Marketing " +
|
||||||
"|| MAI iki/Marketing...",
|
"|| MAI iki/Marketing...",
|
||||||
memberCount = 32,
|
memberCount = Async.Success(32),
|
||||||
isEncrypted = true,
|
isEncrypted = true,
|
||||||
// eventSink = {}
|
// eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
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.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
@ -62,6 +64,7 @@ fun RoomDetailsView(
|
||||||
state: RoomDetailsState,
|
state: RoomDetailsState,
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
onShareRoom: () -> Unit,
|
onShareRoom: () -> Unit,
|
||||||
|
openRoomMemberList: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -87,7 +90,12 @@ fun RoomDetailsView(
|
||||||
TopicSection(roomTopic = state.roomTopic)
|
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) {
|
if (state.isEncrypted) {
|
||||||
SecuritySection()
|
SecuritySection()
|
||||||
|
|
@ -148,12 +156,19 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MembersSection(memberCount: Int, modifier: Modifier = Modifier) {
|
internal fun MembersSection(
|
||||||
|
memberCount: Int?,
|
||||||
|
isLoading: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
openRoomMemberList: () -> Unit
|
||||||
|
) {
|
||||||
PreferenceCategory(modifier = modifier) {
|
PreferenceCategory(modifier = modifier) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(R.string.screen_room_details_people_title),
|
title = stringResource(R.string.screen_room_details_people_title),
|
||||||
icon = Icons.Outlined.Person,
|
icon = Icons.Outlined.Person,
|
||||||
currentValue = memberCount.toString(),
|
currentValue = memberCount?.toString(),
|
||||||
|
onClick = openRoomMemberList,
|
||||||
|
loadingCurrentValue = isLoading,
|
||||||
)
|
)
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(R.string.screen_room_details_invite_people_title),
|
title = stringResource(R.string.screen_room_details_invite_people_title),
|
||||||
|
|
@ -200,5 +215,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
||||||
state = state,
|
state = state,
|
||||||
goBack = {},
|
goBack = {},
|
||||||
onShareRoom = {},
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<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_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||||
<string name="screen_room_details_invite_people_title">"Invite people"</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 app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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.roomName).isEqualTo(room.name)
|
||||||
Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
|
Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
|
||||||
Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic)
|
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)
|
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 {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.roomName).isEqualTo(room.displayName)
|
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 {
|
android {
|
||||||
namespace = "io.element.android.features.selectusers.api"
|
namespace = "io.element.android.features.userlist.api"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.
|
* 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
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
sealed interface SelectUsersEvents {
|
sealed interface UserListEvents {
|
||||||
data class UpdateSearchQuery(val query: String) : SelectUsersEvents
|
data class UpdateSearchQuery(val query: String) : UserListEvents
|
||||||
data class AddToSelection(val matrixUser: MatrixUser) : SelectUsersEvents
|
data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents
|
||||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : SelectUsersEvents
|
data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents
|
||||||
data class OnSearchActiveChanged(val active: Boolean) : SelectUsersEvents
|
data class OnSearchActiveChanged(val active: Boolean) : UserListEvents
|
||||||
}
|
}
|
||||||
|
|
@ -14,13 +14,13 @@
|
||||||
* limitations under the License.
|
* 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
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
interface SelectUsersPresenter : Presenter<SelectUsersState> {
|
interface UserListPresenter : Presenter<UserListState> {
|
||||||
|
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(args: SelectUsersPresenterArgs): SelectUsersPresenter
|
fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
* limitations under the License.
|
* 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,
|
val selectionMode: SelectionMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14,20 +14,20 @@
|
||||||
* limitations under the License.
|
* 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.foundation.lazy.LazyListState
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class SelectUsersState(
|
data class UserListState(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: ImmutableList<MatrixUser>,
|
val searchResults: ImmutableList<MatrixUser>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val selectedUsersListState: LazyListState,
|
val selectedUsersListState: LazyListState,
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
val eventSink: (SelectUsersEvents) -> Unit,
|
val eventSink: (UserListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.foundation.lazy.LazyListState
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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 io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class SelectUsersStateProvider : PreviewParameterProvider<SelectUsersState> {
|
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
override val values: Sequence<SelectUsersState>
|
override val values: Sequence<UserListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aSelectUsersState(),
|
aUserListState(),
|
||||||
aSelectUsersState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aSelectUsersState().copy(isSearchActive = true),
|
aUserListState().copy(isSearchActive = true),
|
||||||
aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone"),
|
aUserListState().copy(isSearchActive = true, searchQuery = "someone"),
|
||||||
aSelectUsersState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple),
|
aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple),
|
||||||
aSelectUsersState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = aListOfResults(),
|
searchResults = aListOfResults(),
|
||||||
),
|
),
|
||||||
aSelectUsersState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
|
|
@ -50,7 +50,7 @@ open class SelectUsersStateProvider : PreviewParameterProvider<SelectUsersState>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aSelectUsersState() = SelectUsersState(
|
fun aUserListState() = UserListState(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
searchResults = persistentListOf(),
|
searchResults = persistentListOf(),
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -65,8 +65,8 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectUsersView(
|
fun UserListView(
|
||||||
state: SelectUsersState,
|
state: UserListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onUserSelected: (MatrixUser) -> Unit = {},
|
onUserSelected: (MatrixUser) -> Unit = {},
|
||||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||||
|
|
@ -82,14 +82,14 @@ fun SelectUsersView(
|
||||||
selectedUsersListState = state.selectedUsersListState,
|
selectedUsersListState = state.selectedUsersListState,
|
||||||
active = state.isSearchActive,
|
active = state.isSearchActive,
|
||||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
||||||
onActiveChanged = { state.eventSink(SelectUsersEvents.OnSearchActiveChanged(it)) },
|
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
|
||||||
onTextChanged = { state.eventSink(SelectUsersEvents.UpdateSearchQuery(it)) },
|
onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
|
||||||
onUserSelected = {
|
onUserSelected = {
|
||||||
state.eventSink(SelectUsersEvents.AddToSelection(it))
|
state.eventSink(UserListEvents.AddToSelection(it))
|
||||||
onUserSelected(it)
|
onUserSelected(it)
|
||||||
},
|
},
|
||||||
onUserDeselected = {
|
onUserDeselected = {
|
||||||
state.eventSink(SelectUsersEvents.RemoveFromSelection(it))
|
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||||
onUserDeselected(it)
|
onUserDeselected(it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -100,7 +100,7 @@ fun SelectUsersView(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
selectedUsers = state.selectedUsers,
|
selectedUsers = state.selectedUsers,
|
||||||
onUserRemoved = {
|
onUserRemoved = {
|
||||||
state.eventSink(SelectUsersEvents.RemoveFromSelection(it))
|
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||||
onUserDeselected(it)
|
onUserDeselected(it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -297,15 +297,15 @@ fun SelectedUser(
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SelectUsersViewLightPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) =
|
internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SelectUsersViewDarkPreview(@PreviewParameter(SelectUsersStateProvider::class) state: SelectUsersState) =
|
internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: SelectUsersState) {
|
private fun ContentToPreview(state: UserListState) {
|
||||||
SelectUsersView(state = state)
|
UserListView(state = state)
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.selectusers.impl"
|
namespace = "io.element.android.features.userlist.impl"
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -41,7 +41,7 @@ dependencies {
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(projects.features.selectusers.api)
|
api(projects.features.userlist.api)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -52,6 +52,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.features.userlist.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -31,10 +31,11 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.selectusers.api.SelectUsersEvents
|
import io.element.android.features.userlist.api.MatrixUserDataSource
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenter
|
import io.element.android.features.userlist.api.UserListEvents
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.features.selectusers.api.SelectUsersState
|
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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -45,18 +46,19 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class DefaultSelectUsersPresenter @AssistedInject constructor(
|
class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Assisted val args: SelectUsersPresenterArgs,
|
@Assisted val args: UserListPresenterArgs,
|
||||||
) : SelectUsersPresenter {
|
@Assisted val matrixUserDataSource: MatrixUserDataSource,
|
||||||
|
) : UserListPresenter {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
interface DefaultSelectUsersFactory : SelectUsersPresenter.Factory {
|
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||||
override fun create(args: SelectUsersPresenterArgs): DefaultSelectUsersPresenter
|
override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): DefaultUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): SelectUsersState {
|
override fun present(): UserListState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers: MutableState<ImmutableList<MatrixUser>> = remember {
|
val selectedUsers: MutableState<ImmutableList<MatrixUser>> = remember {
|
||||||
|
|
@ -68,17 +70,17 @@ class DefaultSelectUsersPresenter @AssistedInject constructor(
|
||||||
mutableStateOf(persistentListOf())
|
mutableStateOf(persistentListOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: SelectUsersEvents) {
|
fun handleEvents(event: UserListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is SelectUsersEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||||
is SelectUsersEvents.UpdateSearchQuery -> searchQuery = event.query
|
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||||
is SelectUsersEvents.AddToSelection -> {
|
is UserListEvents.AddToSelection -> {
|
||||||
if (event.matrixUser !in selectedUsers.value) {
|
if (event.matrixUser !in selectedUsers.value) {
|
||||||
selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList()
|
selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList()
|
||||||
}
|
}
|
||||||
localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState)
|
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,
|
searchQuery = searchQuery,
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults.value,
|
||||||
selectedUsers = selectedUsers.value.reversed().toImmutableList(),
|
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 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 }) {
|
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))
|
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||||
results.add(0, profile)
|
results.add(0, profile)
|
||||||
}
|
}
|
||||||
|
|
@ -14,16 +14,17 @@
|
||||||
* limitations under the License.
|
* 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.LazyListState
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.selectusers.api.SelectUsersEvents
|
import io.element.android.features.userlist.api.UserListEvents
|
||||||
import io.element.android.features.selectusers.api.SelectUsersPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.features.selectusers.api.SelectionMode
|
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.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
@ -34,11 +35,16 @@ import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class DefaultSelectUsersPresenterTests {
|
class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
|
private val userListDataSource = FakeMatrixUserDataSource()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for single selection`() = runTest {
|
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) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -53,7 +59,10 @@ class DefaultSelectUsersPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for multiple selection`() = runTest {
|
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) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -68,26 +77,29 @@ class DefaultSelectUsersPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - update search query`() = runTest {
|
fun `present - update search query`() = runTest {
|
||||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single))
|
val presenter = DefaultUserListPresenter(
|
||||||
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
|
userListDataSource
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(true))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(true))
|
||||||
assertThat(awaitItem().isSearchActive).isTrue()
|
assertThat(awaitItem().isSearchActive).isTrue()
|
||||||
|
|
||||||
val matrixIdQuery = "@name:matrix.org"
|
val matrixIdQuery = "@name:matrix.org"
|
||||||
initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(matrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(matrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(matrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery)))
|
assertThat(awaitItem().searchResults).containsExactly(MatrixUser(UserId(matrixIdQuery)))
|
||||||
|
|
||||||
val notMatrixIdQuery = "name"
|
val notMatrixIdQuery = "name"
|
||||||
initialState.eventSink(SelectUsersEvents.UpdateSearchQuery(notMatrixIdQuery))
|
initialState.eventSink(UserListEvents.UpdateSearchQuery(notMatrixIdQuery))
|
||||||
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
assertThat(awaitItem().searchQuery).isEqualTo(notMatrixIdQuery)
|
||||||
assertThat(awaitItem().searchResults).isEmpty()
|
assertThat(awaitItem().searchResults).isEmpty()
|
||||||
|
|
||||||
initialState.eventSink(SelectUsersEvents.OnSearchActiveChanged(false))
|
initialState.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
assertThat(awaitItem().isSearchActive).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +109,10 @@ class DefaultSelectUsersPresenterTests {
|
||||||
mockkConstructor(LazyListState::class)
|
mockkConstructor(LazyListState::class)
|
||||||
coJustRun { anyConstructed<LazyListState>().scrollToItem(index = any()) }
|
coJustRun { anyConstructed<LazyListState>().scrollToItem(index = any()) }
|
||||||
|
|
||||||
val presenter = DefaultSelectUsersPresenter(SelectUsersPresenterArgs(SelectionMode.Single))
|
val presenter = DefaultUserListPresenter(
|
||||||
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
|
userListDataSource
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -108,23 +123,23 @@ class DefaultSelectUsersPresenterTests {
|
||||||
val userABis = aMatrixUser("userA", "A")
|
val userABis = aMatrixUser("userA", "A")
|
||||||
val userC = aMatrixUser("userC", "C")
|
val userC = aMatrixUser("userC", "C")
|
||||||
|
|
||||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userA))
|
initialState.eventSink(UserListEvents.AddToSelection(userA))
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userA)
|
||||||
|
|
||||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userB))
|
initialState.eventSink(UserListEvents.AddToSelection(userB))
|
||||||
// the last added user should be presented first
|
// the last added user should be presented first
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userB, userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userB, userA)
|
||||||
|
|
||||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userABis))
|
initialState.eventSink(UserListEvents.AddToSelection(userABis))
|
||||||
initialState.eventSink(SelectUsersEvents.AddToSelection(userC))
|
initialState.eventSink(UserListEvents.AddToSelection(userC))
|
||||||
// duplicated users should be ignored
|
// duplicated users should be ignored
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA)
|
||||||
|
|
||||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userB))
|
initialState.eventSink(UserListEvents.RemoveFromSelection(userB))
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userC, userA)
|
||||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userA))
|
initialState.eventSink(UserListEvents.RemoveFromSelection(userA))
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userC)
|
assertThat(awaitItem().selectedUsers).containsExactly(userC)
|
||||||
initialState.eventSink(SelectUsersEvents.RemoveFromSelection(userC))
|
initialState.eventSink(UserListEvents.RemoveFromSelection(userC))
|
||||||
assertThat(awaitItem().selectedUsers).isEmpty()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,3 +57,7 @@ suspend fun <T> (suspend () -> Result<T>).executeResult(state: MutableState<Asyn
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> Async<T>.isLoading(): Boolean {
|
||||||
|
return this is Async.Loading<T>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.progressSemantics
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -39,6 +41,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
|
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
|
||||||
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.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -47,6 +50,7 @@ fun PreferenceText(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
subtitle: String? = null,
|
subtitle: String? = null,
|
||||||
currentValue: String? = null,
|
currentValue: String? = null,
|
||||||
|
loadingCurrentValue: Boolean = false,
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
tintColor: Color? = null,
|
tintColor: Color? = null,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
|
|
@ -56,11 +60,13 @@ fun PreferenceText(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.defaultMinSize(minHeight = minHeight)
|
.defaultMinSize(minHeight = minHeight)
|
||||||
.padding(end = preferencePaddingHorizontal)
|
.clickable { onClick() }
|
||||||
.clickable { onClick() },
|
.padding(end = preferencePaddingHorizontal),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = preferencePaddingVertical)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = preferencePaddingVertical)
|
||||||
) {
|
) {
|
||||||
PreferenceIcon(icon = icon, tintColor = tintColor)
|
PreferenceIcon(icon = icon, tintColor = tintColor)
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
|
|
@ -88,7 +94,11 @@ fun PreferenceText(
|
||||||
if (currentValue != null) {
|
if (currentValue != null) {
|
||||||
Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary)
|
Text(currentValue, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary)
|
||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
|
} else if (loadingCurrentValue) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.progressSemantics().size(20.dp), strokeWidth = 2.dp)
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ interface MatrixRoom: Closeable {
|
||||||
val alternativeAliases: List<String>
|
val alternativeAliases: List<String>
|
||||||
val topic: String?
|
val topic: String?
|
||||||
val avatarUrl: String?
|
val avatarUrl: String?
|
||||||
val members: List<RoomMember>
|
|
||||||
val isEncrypted: Boolean
|
val isEncrypted: Boolean
|
||||||
|
|
||||||
|
suspend fun members() : List<RoomMember>
|
||||||
|
suspend fun memberCount(): Int
|
||||||
|
|
||||||
fun syncUpdateFlow(): Flow<Long>
|
fun syncUpdateFlow(): Flow<Long>
|
||||||
|
|
||||||
fun timeline(): MatrixTimeline
|
fun timeline(): MatrixTimeline
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.impl.room
|
package io.element.android.libraries.matrix.impl.room
|
||||||
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
@ -24,10 +25,12 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
||||||
|
|
@ -43,10 +46,32 @@ class RustMatrixRoom(
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : MatrixRoom {
|
) : MatrixRoom {
|
||||||
|
|
||||||
|
private var loadMembersJob: Job? = null
|
||||||
|
private var cachedMembers: List<RoomMember> = emptyList()
|
||||||
|
|
||||||
|
override suspend fun members(): List<RoomMember> {
|
||||||
|
return cachedMembers.ifEmpty {
|
||||||
|
if (loadMembersJob == null) {
|
||||||
|
loadMembersJob = coroutineScope.launch(coroutineDispatchers.io) {
|
||||||
|
cachedMembers = tryOrNull {
|
||||||
|
innerRoom.members().map(RoomMemberMapper::map)
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadMembersJob?.join()
|
||||||
|
loadMembersJob = null
|
||||||
|
cachedMembers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun memberCount(): Int {
|
||||||
|
return members().size
|
||||||
|
}
|
||||||
|
|
||||||
override fun syncUpdateFlow(): Flow<Long> {
|
override fun syncUpdateFlow(): Flow<Long> {
|
||||||
return slidingSyncUpdateFlow
|
return slidingSyncUpdateFlow
|
||||||
.filter {
|
.filter {
|
||||||
it.rooms.contains(innerRoom.id())
|
it.rooms.contains(roomId.value)
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
|
|
@ -95,9 +120,6 @@ class RustMatrixRoom(
|
||||||
return innerRoom.avatarUrl()
|
return innerRoom.avatarUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val members: List<RoomMember>
|
|
||||||
get() = innerRoom.members().map(RoomMemberMapper::map)
|
|
||||||
|
|
||||||
override val isEncrypted: Boolean
|
override val isEncrypted: Boolean
|
||||||
get() = innerRoom.isEncrypted()
|
get() = innerRoom.isEncrypted()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,14 +87,6 @@ class RustMatrixTimeline(
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
Timber.v("Init timeline for room ${matrixRoom.roomId}")
|
Timber.v("Init timeline for room ${matrixRoom.roomId}")
|
||||||
coroutineScope.launch {
|
|
||||||
matrixRoom.fetchMembers()
|
|
||||||
.onFailure {
|
|
||||||
Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}")
|
|
||||||
}.onSuccess {
|
|
||||||
Timber.v("Success fetching members for room ${matrixRoom.roomId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val result = addListener(innerTimelineListener)
|
val result = addListener(innerTimelineListener)
|
||||||
result
|
result
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,18 @@ class FakeMatrixRoom(
|
||||||
override val displayName: String = "",
|
override val displayName: String = "",
|
||||||
override val topic: String? = null,
|
override val topic: String? = null,
|
||||||
override val avatarUrl: String? = null,
|
override val avatarUrl: String? = null,
|
||||||
override val members: List<RoomMember> = emptyList(),
|
|
||||||
override val isEncrypted: Boolean = false,
|
override val isEncrypted: Boolean = false,
|
||||||
override val alias: String? = null,
|
override val alias: String? = null,
|
||||||
override val alternativeAliases: List<String> = emptyList(),
|
override val alternativeAliases: List<String> = emptyList(),
|
||||||
|
private val members: List<RoomMember> = emptyList(),
|
||||||
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||||
) : MatrixRoom {
|
) : MatrixRoom {
|
||||||
|
|
||||||
|
private var fetchMemberResult: Result<Unit> = Result.success(Unit)
|
||||||
|
|
||||||
|
var areMembersFetched: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
override fun syncUpdateFlow(): Flow<Long> {
|
override fun syncUpdateFlow(): Flow<Long> {
|
||||||
return emptyFlow()
|
return emptyFlow()
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +55,11 @@ class FakeMatrixRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchMembers(): Result<Unit> {
|
override suspend fun fetchMembers(): Result<Unit> {
|
||||||
return Result.success(Unit)
|
return fetchMemberResult.also { result ->
|
||||||
|
if (result.isSuccess) {
|
||||||
|
areMembersFetched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun userDisplayName(userId: String): Result<String?> {
|
override suspend fun userDisplayName(userId: String): Result<String?> {
|
||||||
|
|
@ -61,6 +70,18 @@ class FakeMatrixRoom(
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun members(): List<RoomMember> {
|
||||||
|
return members
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun memberCount(): Int {
|
||||||
|
if (fetchMemberResult.isSuccess) {
|
||||||
|
return members.count()
|
||||||
|
} else {
|
||||||
|
throw fetchMemberResult.exceptionOrNull()!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(message: String): Result<Unit> {
|
override suspend fun sendMessage(message: String): Result<Unit> {
|
||||||
delay(100)
|
delay(100)
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
|
|
@ -94,4 +115,8 @@ class FakeMatrixRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
|
||||||
|
fun givenFetchMemberResult(result: Result<Unit>) {
|
||||||
|
fetchMemberResult = result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
libraries/ui-strings/src/main/res/values-ro/translations.xml
Normal file
10
libraries/ui-strings/src/main/res/values-ro/translations.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="action_confirm">"Confirmare"</string>
|
||||||
|
<string name="action_create_a_room">"Creați o cameră"</string>
|
||||||
|
<string name="action_done">"Gata"</string>
|
||||||
|
<string name="action_ok">"OK"</string>
|
||||||
|
<string name="action_report_content">"Raportează conținutul"</string>
|
||||||
|
<string name="action_start_chat">"Începe discuția"</string>
|
||||||
|
<string name="action_view_source">"Vezi sursa"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
<string name="common_about">"About"</string>
|
<string name="common_about">"About"</string>
|
||||||
<string name="common_audio">"Audio"</string>
|
<string name="common_audio">"Audio"</string>
|
||||||
<string name="common_bubbles">"Bubbles"</string>
|
<string name="common_bubbles">"Bubbles"</string>
|
||||||
|
<string name="common_creating_room">"Creating room…"</string>
|
||||||
<string name="common_decryption_error">"Decryption error"</string>
|
<string name="common_decryption_error">"Decryption error"</string>
|
||||||
<string name="common_developer_options">"Developer options"</string>
|
<string name="common_developer_options">"Developer options"</string>
|
||||||
<string name="common_edited_suffix">"(edited)"</string>
|
<string name="common_edited_suffix">"(edited)"</string>
|
||||||
|
|
@ -122,11 +123,19 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||||
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
|
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
|
||||||
<string name="report_content_explanation">"Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."</string>
|
<string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
|
||||||
<string name="report_content_hint">"Reason for reporting this content"</string>
|
<string name="report_content_hint">"Reason for reporting this content"</string>
|
||||||
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
|
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
|
||||||
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
||||||
<string name="room_timeline_read_marker_title">"New"</string>
|
<string name="room_timeline_read_marker_title">"New"</string>
|
||||||
|
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
||||||
|
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||||
|
<string name="screen_dm_details_block_user">"Block user"</string>
|
||||||
|
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
|
||||||
|
<string name="screen_dm_details_unblock_alert_description">"On unblocking the user, you will be able to see all messages by them again."</string>
|
||||||
|
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
|
||||||
|
<string name="screen_report_content_block_user">"Block user"</string>
|
||||||
|
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||||
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
||||||
<string name="screen_room_member_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
<string name="screen_room_member_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||||
<string name="screen_room_member_details_block_user">"Block user"</string>
|
<string name="screen_room_member_details_block_user">"Block user"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6893108aabbf06f43af90f17b6b1a8a521312559f407647ce08db06ecb9a8f84
|
||||||
|
size 22033
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7e1f7feb544a84e6e66f995683038abb9e7465583fade4e77c254a4264d03b9f
|
||||||
|
size 11985
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3e7b5bd916d4d3067b5013400b4ac864fab560e96fcb75ec895684021a00b8ba
|
||||||
|
size 21808
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:309c0c0a650435cfbc0825025f6f852bdb51899ff9d37cf4cf9db00d56fb1176
|
||||||
|
size 11933
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9fe9b98fbdd3fd9c7789463f14345f6f65f9a18eedf2229d14c62a3bee6711e3
|
||||||
|
size 70058
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:48f138bc8a63ac25152491c974c68d6e024ded04b973f174fb5b9586523217ca
|
||||||
|
size 64593
|
||||||
|
|
@ -61,7 +61,9 @@
|
||||||
{
|
{
|
||||||
"name": ":features:roomdetails:impl",
|
"name": ":features:roomdetails:impl",
|
||||||
"includeRegex": [
|
"includeRegex": [
|
||||||
"screen_room_details_.*"
|
"screen_room_details_.*",
|
||||||
|
"screen_room_member_list_.*",
|
||||||
|
"screen_dm_details_.*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue