refactor (start chat) : create invite people module and start branching them
This commit is contained in:
parent
1170a44116
commit
50073389c4
21 changed files with 717 additions and 32 deletions
|
|
@ -23,7 +23,7 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
|
setupAnvil()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
|
@ -41,6 +41,7 @@ dependencies {
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
|
implementation(projects.features.invitepeople.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,32 @@
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
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.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.createroom.impl.addpeople.AddPeopleNode
|
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
||||||
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class CreateRoomFlowNode @AssistedInject constructor(
|
class CreateRoomFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val client: MatrixClient,
|
||||||
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.ConfigureRoom,
|
initialElement = NavTarget.ConfigureRoom,
|
||||||
|
|
@ -35,13 +42,30 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.ConfigureRoom -> createNode<ConfigureRoomNode>(buildContext)
|
NavTarget.ConfigureRoom -> {
|
||||||
is NavTarget.AddPeople -> createNode<AddPeopleNode>(buildContext)
|
val callback = object : ConfigureRoomNode.Callback {
|
||||||
|
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||||
|
backstack.push(NavTarget.AddPeople(roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<ConfigureRoomNode>(buildContext, plugins = listOf(callback))
|
||||||
|
}
|
||||||
|
is NavTarget.AddPeople -> {
|
||||||
|
val joinedRoom = runBlocking { client.getJoinedRoom(navTarget.roomId) } ?: error("Room not found")
|
||||||
|
val inputs = AddPeopleNode.Inputs(joinedRoom)
|
||||||
|
createNode<AddPeopleNode>(buildContext, plugins = listOf(inputs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
BackstackView()
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object ConfigureRoom : NavTarget
|
data object ConfigureRoom : NavTarget
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
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.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultCreateRoomEntryPoint @Inject constructor(): CreateRoomEntryPoint {
|
class DefaultCreateRoomEntryPoint @Inject constructor(): CreateRoomEntryPoint {
|
||||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext)
|
return parentNode.createNode<CreateRoomFlowNode>(buildContext)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,40 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.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.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
class AddPeopleNode(
|
@ContributesNode(SessionScope::class)
|
||||||
buildContext: BuildContext,
|
class AddPeopleNode @AssistedInject constructor(
|
||||||
plugins: List<Plugin>,
|
@Assisted buildContext: BuildContext,
|
||||||
) : Node(buildContext, plugins = plugins)
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
|
||||||
|
private val invitePeopleRenderer: InvitePeopleRenderer,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
data class Inputs(
|
||||||
|
val joinedRoom: JoinedRoom
|
||||||
|
): NodeInputs
|
||||||
|
|
||||||
|
private val joinedRoom = inputs<Inputs>().joinedRoom
|
||||||
|
private val invitePeoplePresenter = invitePeoplePresenterFactory.create(joinedRoom)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = invitePeoplePresenter.present()
|
||||||
|
invitePeopleRenderer.Render(state, Modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,14 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
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 im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -28,6 +31,10 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback: Plugin {
|
||||||
|
fun onCreateRoomSuccess(roomId: RoomId)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onResume = {
|
onResume = {
|
||||||
|
|
@ -36,6 +43,10 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onCreateRoomSuccess(roomId: RoomId){
|
||||||
|
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -43,9 +54,7 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = this::navigateUp,
|
onBackClick = this::navigateUp,
|
||||||
onCreateRoomSuccess = {
|
onCreateRoomSuccess = ::onCreateRoomSuccess,
|
||||||
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
|
interface InvitePeopleEvents {
|
||||||
|
data object SendInvites : InvitePeopleEvents
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,13 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
||||||
|
interface InvitePeoplePresenter : Presenter<InvitePeopleState> {
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
fun create(room: JoinedRoom): InvitePeoplePresenter
|
||||||
|
}
|
||||||
|
|
||||||
interface InvitePeoplePresenter {
|
|
||||||
@Composable
|
|
||||||
fun present(): InvitePeopleState
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,8 @@ import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
interface InvitePeopleRenderer {
|
interface InvitePeopleRenderer {
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(state: InvitePeopleState, modifier: Modifier)
|
fun Render(
|
||||||
|
state: InvitePeopleState,
|
||||||
|
modifier: Modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
interface InvitePeopleState
|
interface InvitePeopleState {
|
||||||
|
val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import extension.ComponentMergingStrategy
|
|
||||||
import extension.setupAnvil
|
import extension.setupAnvil
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -23,7 +22,7 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
|
setupAnvil()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
|
@ -33,14 +32,8 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.deeplink)
|
|
||||||
implementation(projects.libraries.mediapickers.api)
|
|
||||||
implementation(projects.libraries.mediaupload.api)
|
|
||||||
implementation(projects.libraries.permissions.api)
|
|
||||||
implementation(projects.libraries.usersearch.impl)
|
implementation(projects.libraries.usersearch.impl)
|
||||||
implementation(projects.services.analytics.api)
|
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.featureflag.api)
|
|
||||||
api(projects.features.invitepeople.api)
|
api(projects.features.invitepeople.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -50,14 +43,8 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.services.analytics.test)
|
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
|
||||||
testImplementation(projects.libraries.permissions.test)
|
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
testImplementation(projects.features.startchat.test)
|
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
|
sealed interface DefaultInvitePeopleEvents: InvitePeopleEvents {
|
||||||
|
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
|
||||||
|
data class UpdateSearchQuery(val query: String) : DefaultInvitePeopleEvents
|
||||||
|
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.room.filterMembers
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class DefaultInvitePeoplePresenter @AssistedInject constructor(
|
||||||
|
@Assisted private val room: JoinedRoom,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : InvitePeoplePresenter {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
interface Factory : InvitePeoplePresenter.Factory {
|
||||||
|
override fun create(room: JoinedRoom): DefaultInvitePeoplePresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): InvitePeopleState {
|
||||||
|
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
|
||||||
|
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
||||||
|
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
||||||
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
fetchMembers(roomMembers)
|
||||||
|
}
|
||||||
|
LaunchedEffect(searchQuery, roomMembers) {
|
||||||
|
performSearch(
|
||||||
|
searchResults = searchResults,
|
||||||
|
roomMembers = roomMembers,
|
||||||
|
selectedUsers = selectedUsers,
|
||||||
|
showSearchLoader = showSearchLoader,
|
||||||
|
searchQuery = searchQuery
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultInvitePeopleState(
|
||||||
|
canInvite = selectedUsers.value.isNotEmpty(),
|
||||||
|
selectedUsers = selectedUsers.value,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isSearchActive = searchActive,
|
||||||
|
searchResults = searchResults.value,
|
||||||
|
showSearchLoader = showSearchLoader.value,
|
||||||
|
eventSink = {
|
||||||
|
when (it) {
|
||||||
|
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
|
||||||
|
searchActive = it.active
|
||||||
|
searchQuery = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
|
||||||
|
searchQuery = it.query
|
||||||
|
}
|
||||||
|
|
||||||
|
is DefaultInvitePeopleEvents.ToggleUser -> {
|
||||||
|
selectedUsers.toggleUser(it.user)
|
||||||
|
searchResults.toggleUser(it.user)
|
||||||
|
}
|
||||||
|
is InvitePeopleEvents.SendInvites -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmName("toggleUserInSelectedUsers")
|
||||||
|
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
|
||||||
|
value = if (value.contains(user)) {
|
||||||
|
value.filterNot { it.userId == user.userId }
|
||||||
|
} else {
|
||||||
|
value + user
|
||||||
|
}.toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmName("toggleUserInSearchResults")
|
||||||
|
private fun MutableState<SearchBarResultState<ImmutableList<InvitableUser>>>.toggleUser(user: MatrixUser) {
|
||||||
|
val existingResults = value
|
||||||
|
if (existingResults is SearchBarResultState.Results) {
|
||||||
|
value = SearchBarResultState.Results(
|
||||||
|
existingResults.results.map { iu ->
|
||||||
|
if (iu.matrixUser == user) {
|
||||||
|
iu.copy(isSelected = !iu.isSelected)
|
||||||
|
} else {
|
||||||
|
iu
|
||||||
|
}
|
||||||
|
}.toImmutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun performSearch(
|
||||||
|
searchResults: MutableState<SearchBarResultState<ImmutableList<InvitableUser>>>,
|
||||||
|
roomMembers: MutableState<AsyncData<ImmutableList<RoomMember>>>,
|
||||||
|
selectedUsers: MutableState<ImmutableList<MatrixUser>>,
|
||||||
|
showSearchLoader: MutableState<Boolean>,
|
||||||
|
searchQuery: String,
|
||||||
|
) = withContext(coroutineDispatchers.io) {
|
||||||
|
searchResults.value = SearchBarResultState.Initial()
|
||||||
|
showSearchLoader.value = false
|
||||||
|
val joinedMembers = roomMembers.value.dataOrNull().orEmpty()
|
||||||
|
|
||||||
|
userRepository.search(searchQuery).onEach { state ->
|
||||||
|
showSearchLoader.value = state.isSearching
|
||||||
|
searchResults.value = when {
|
||||||
|
state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial()
|
||||||
|
state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound()
|
||||||
|
else -> SearchBarResultState.Results(state.results.map { result ->
|
||||||
|
val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
|
||||||
|
val isJoined = existingMembership == RoomMembershipState.JOIN
|
||||||
|
val isInvited = existingMembership == RoomMembershipState.INVITE
|
||||||
|
InvitableUser(
|
||||||
|
matrixUser = result.matrixUser,
|
||||||
|
isSelected = selectedUsers.value.contains(result.matrixUser) || isJoined || isInvited,
|
||||||
|
isAlreadyJoined = isJoined,
|
||||||
|
isAlreadyInvited = isInvited,
|
||||||
|
isUnresolved = result.isUnresolved,
|
||||||
|
)
|
||||||
|
}.toImmutableList())
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchMembers(roomMembers: MutableState<AsyncData<ImmutableList<RoomMember>>>) {
|
||||||
|
suspend {
|
||||||
|
room.filterMembers("", coroutineDispatchers.io).toImmutableList()
|
||||||
|
}.runCatchingUpdatingState(roomMembers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultInvitePeopleRenderer @Inject constructor() : InvitePeopleRenderer {
|
||||||
|
@Composable
|
||||||
|
override fun Render(state: InvitePeopleState, modifier: Modifier) {
|
||||||
|
if (state is DefaultInvitePeopleState) {
|
||||||
|
InvitePeopleView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = {},
|
||||||
|
onSubmitClick = {},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
error("Unsupported state type: ${state::javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class DefaultInvitePeopleState(
|
||||||
|
val canInvite: Boolean,
|
||||||
|
val searchQuery: String,
|
||||||
|
val showSearchLoader: Boolean,
|
||||||
|
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||||
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
|
val isSearchActive: Boolean,
|
||||||
|
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
|
): InvitePeopleState
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<DefaultInvitePeopleState> {
|
||||||
|
override val values: Sequence<DefaultInvitePeopleState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aDefaultInvitePeopleState(),
|
||||||
|
aDefaultInvitePeopleState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
|
||||||
|
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query"),
|
||||||
|
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
|
||||||
|
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
|
||||||
|
aDefaultInvitePeopleState(
|
||||||
|
isSearchActive = true,
|
||||||
|
canInvite = true,
|
||||||
|
searchQuery = "some query",
|
||||||
|
selectedUsers = persistentListOf(
|
||||||
|
aMatrixUser("@carol:server.org", "Carol")
|
||||||
|
),
|
||||||
|
searchResults = SearchBarResultState.Results(
|
||||||
|
persistentListOf(
|
||||||
|
InvitableUser(aMatrixUser("@alice:server.org")),
|
||||||
|
InvitableUser(aMatrixUser("@bob:server.org", "Bob")),
|
||||||
|
InvitableUser(aMatrixUser("@carol:server.org", "Carol"), isSelected = true),
|
||||||
|
InvitableUser(aMatrixUser("@eve:server.org", "Eve"), isSelected = true, isAlreadyJoined = true),
|
||||||
|
InvitableUser(aMatrixUser("@justin:server.org", "Justin"), isSelected = true, isAlreadyInvited = true),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aDefaultInvitePeopleState(
|
||||||
|
isSearchActive = true,
|
||||||
|
canInvite = true,
|
||||||
|
searchQuery = "@alice:server.org",
|
||||||
|
selectedUsers = persistentListOf(
|
||||||
|
aMatrixUser("@carol:server.org", "Carol")
|
||||||
|
),
|
||||||
|
searchResults = SearchBarResultState.Results(
|
||||||
|
persistentListOf(
|
||||||
|
InvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
|
||||||
|
InvitableUser(aMatrixUser("@bob:server.org", "Bob")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aDefaultInvitePeopleState(
|
||||||
|
isSearchActive = true,
|
||||||
|
canInvite = true,
|
||||||
|
searchQuery = "@alice:server.org",
|
||||||
|
searchResults = SearchBarResultState.Results(
|
||||||
|
persistentListOf(
|
||||||
|
InvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
showSearchLoader = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aDefaultInvitePeopleState(
|
||||||
|
canInvite: Boolean = false,
|
||||||
|
searchQuery: String = "",
|
||||||
|
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.Initial(),
|
||||||
|
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
|
isSearchActive: Boolean = false,
|
||||||
|
showSearchLoader: Boolean = false,
|
||||||
|
): DefaultInvitePeopleState {
|
||||||
|
return DefaultInvitePeopleState(
|
||||||
|
canInvite = canInvite,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
searchResults = searchResults,
|
||||||
|
selectedUsers = selectedUsers,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
showSearchLoader = showSearchLoader,
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
|
data class InvitableUser(
|
||||||
|
val matrixUser: MatrixUser,
|
||||||
|
val isSelected: Boolean = false,
|
||||||
|
val isAlreadyJoined: Boolean = false,
|
||||||
|
val isAlreadyInvited: Boolean = false,
|
||||||
|
val isUnresolved: Boolean = false,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
|
||||||
|
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InvitePeopleView(
|
||||||
|
state: DefaultInvitePeopleState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSubmitClick: (List<MatrixUser>) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
RoomInviteMembersTopBar(
|
||||||
|
onBackClick = {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
state.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(false))
|
||||||
|
} else {
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmitClick = { onSubmitClick(state.selectedUsers) },
|
||||||
|
canSend = state.canInvite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
RoomInviteMembersSearchBar(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
query = state.searchQuery,
|
||||||
|
showLoader = state.showSearchLoader,
|
||||||
|
selectedUsers = state.selectedUsers,
|
||||||
|
state = state.searchResults,
|
||||||
|
active = state.isSearchActive,
|
||||||
|
onActiveChange = {
|
||||||
|
state.eventSink(
|
||||||
|
DefaultInvitePeopleEvents.OnSearchActiveChanged(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onTextChange = { state.eventSink(DefaultInvitePeopleEvents.UpdateSearchQuery(it)) },
|
||||||
|
onToggleUser = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!state.isSearchActive) {
|
||||||
|
SelectedUsersRowList(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
selectedUsers = state.selectedUsers,
|
||||||
|
autoScroll = true,
|
||||||
|
onUserRemove = { state.eventSink(DefaultInvitePeopleEvents.ToggleUser(it)) },
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RoomInviteMembersTopBar(
|
||||||
|
canSend: Boolean,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSubmitClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
titleStr = "Invite people",
|
||||||
|
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(CommonStrings.action_invite),
|
||||||
|
onClick = onSubmitClick,
|
||||||
|
enabled = canSend,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RoomInviteMembersSearchBar(
|
||||||
|
query: String,
|
||||||
|
state: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||||
|
showLoader: Boolean,
|
||||||
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
|
active: Boolean,
|
||||||
|
onActiveChange: (Boolean) -> Unit,
|
||||||
|
onTextChange: (String) -> Unit,
|
||||||
|
onToggleUser: (MatrixUser) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onTextChange,
|
||||||
|
active = active,
|
||||||
|
onActiveChange = onActiveChange,
|
||||||
|
modifier = modifier,
|
||||||
|
placeHolderTitle = placeHolderTitle,
|
||||||
|
contentPrefix = {
|
||||||
|
if (selectedUsers.isNotEmpty()) {
|
||||||
|
SelectedUsersRowList(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
selectedUsers = selectedUsers,
|
||||||
|
autoScroll = true,
|
||||||
|
onUserRemove = onToggleUser,
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showBackButton = false,
|
||||||
|
resultState = state,
|
||||||
|
contentSuffix = {
|
||||||
|
if (showLoader) {
|
||||||
|
AsyncLoading()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resultHandler = { results ->
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = CommonStrings.common_search_results),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn {
|
||||||
|
itemsIndexed(results) { index, invitableUser ->
|
||||||
|
val notInvitedOrJoined =
|
||||||
|
!(invitableUser.isAlreadyInvited || invitableUser.isAlreadyJoined)
|
||||||
|
val isUnresolved = invitableUser.isUnresolved && notInvitedOrJoined
|
||||||
|
val enabled = isUnresolved || notInvitedOrJoined
|
||||||
|
val data = if (isUnresolved) {
|
||||||
|
CheckableUserRowData.Unresolved(
|
||||||
|
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
|
id = invitableUser.matrixUser.userId.value,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CheckableUserRowData.Resolved(
|
||||||
|
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||||
|
name = invitableUser.matrixUser.getBestName(),
|
||||||
|
subtext = when {
|
||||||
|
// If they're already invited or joined we show that information
|
||||||
|
invitableUser.isAlreadyJoined -> "Already a member"
|
||||||
|
invitableUser.isAlreadyInvited -> "Already invited"
|
||||||
|
// Otherwise show the ID, unless that's already used for their name
|
||||||
|
invitableUser.matrixUser.displayName.isNullOrEmpty()
|
||||||
|
.not() -> invitableUser.matrixUser.userId.value
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CheckableUserRow(
|
||||||
|
checked = invitableUser.isSelected,
|
||||||
|
enabled = enabled,
|
||||||
|
data = data,
|
||||||
|
onCheckedChange = { onToggleUser(invitableUser.matrixUser) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index < results.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun RoomInviteMembersViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =
|
||||||
|
ElementPreview {
|
||||||
|
InvitePeopleView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = {},
|
||||||
|
onSubmitClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,7 @@ dependencies {
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
|
implementation(projects.features.createroom.api)
|
||||||
api(projects.features.startchat.api)
|
api(projects.features.startchat.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
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.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.startchat.DefaultStartChatNavigator
|
import io.element.android.features.startchat.DefaultStartChatNavigator
|
||||||
import io.element.android.features.startchat.api.StartChatEntryPoint
|
import io.element.android.features.startchat.api.StartChatEntryPoint
|
||||||
import io.element.android.features.startchat.impl.joinbyaddress.JoinRoomByAddressNode
|
import io.element.android.features.startchat.impl.joinbyaddress.JoinRoomByAddressNode
|
||||||
|
|
@ -36,6 +37,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class StartChatFlowNode @AssistedInject constructor(
|
class StartChatFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
) : BaseFlowNode<StartChatFlowNode.NavTarget>(
|
) : BaseFlowNode<StartChatFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -72,7 +74,7 @@ class StartChatFlowNode @AssistedInject constructor(
|
||||||
createNode<StartChatNode>(buildContext = buildContext, plugins = listOf(navigator))
|
createNode<StartChatNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
}
|
}
|
||||||
NavTarget.NewRoom -> {
|
NavTarget.NewRoom -> {
|
||||||
createNode<CreateRoomFlowNode>(buildContext = buildContext, plugins = listOf(navigator))
|
createRoomEntryPoint.createNode(parentNode = this, buildContext = buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.JoinByAddress -> {
|
NavTarget.JoinByAddress -> {
|
||||||
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to filter members by userId or displayName.
|
||||||
|
* It does filter through the already known members, it doesn't perform additional requests.
|
||||||
|
*/
|
||||||
|
suspend fun BaseRoom.filterMembers(query: String, coroutineContext: CoroutineContext): List<RoomMember> = withContext(coroutineContext) {
|
||||||
|
val roomMembersState = membersStateFlow.value
|
||||||
|
val activeRoomMembers = roomMembersState.roomMembers()
|
||||||
|
?.filter { it.membership.isActive() }
|
||||||
|
.orEmpty()
|
||||||
|
val filteredMembers = if (query.isBlank()) {
|
||||||
|
activeRoomMembers
|
||||||
|
} else {
|
||||||
|
activeRoomMembers.filter { member ->
|
||||||
|
member.userId.value.contains(query, ignoreCase = true) ||
|
||||||
|
member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filteredMembers
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue