refactor (start chat) : use invite people module in room details screen
This commit is contained in:
parent
bfd1182baf
commit
74f6a83219
20 changed files with 207 additions and 543 deletions
|
|
@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
|
|
@ -48,7 +49,7 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
|||
NavTarget.ConfigureRoom -> {
|
||||
val callback = object : ConfigureRoomNode.Callback {
|
||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||
backstack.push(NavTarget.AddPeople(roomId))
|
||||
backstack.replace(NavTarget.AddPeople(roomId))
|
||||
}
|
||||
}
|
||||
createNode<ConfigureRoomNode>(buildContext, plugins = listOf(callback))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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 com.squareup.anvil.annotations.ContributesBinding
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
|
|
@ -27,13 +26,13 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
|||
class AddPeopleNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
|
||||
invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
|
||||
private val invitePeopleRenderer: InvitePeopleRenderer,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
data class Inputs(
|
||||
val joinedRoom: JoinedRoom
|
||||
): NodeInputs
|
||||
) : NodeInputs
|
||||
|
||||
private val joinedRoom = inputs<Inputs>().joinedRoom
|
||||
private val invitePeoplePresenter = invitePeoplePresenterFactory.create(joinedRoom)
|
||||
|
|
@ -41,6 +40,10 @@ class AddPeopleNode @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = invitePeoplePresenter.present()
|
||||
invitePeopleRenderer.Render(state, Modifier)
|
||||
AddPeopleView(
|
||||
state = state,
|
||||
invitePeopleView = { invitePeopleRenderer.Render(state, Modifier) },
|
||||
onFinish = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.createroom.impl.addpeople
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun AddPeopleView(
|
||||
state: InvitePeopleState,
|
||||
invitePeopleView: @Composable () -> Unit,
|
||||
onFinish: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
topBar = {
|
||||
AddPeopleTopBar(onSkipClick = onFinish)
|
||||
},
|
||||
footer = {
|
||||
Button(
|
||||
text = "Finish",
|
||||
onClick = { state.eventSink(InvitePeopleEvents.SendInvites) },
|
||||
enabled = state.canInvite,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
},
|
||||
content = invitePeopleView
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AddPeopleTopBar(
|
||||
onSkipClick: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
titleStr = "Invite people",
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_skip),
|
||||
onClick = onSkipClick,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -9,4 +9,5 @@ package io.element.android.features.invitepeople.api
|
|||
|
||||
interface InvitePeopleEvents {
|
||||
data object SendInvites : InvitePeopleEvents
|
||||
data object CloseSearch : InvitePeopleEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@
|
|||
package io.element.android.features.invitepeople.api
|
||||
|
||||
interface InvitePeopleState {
|
||||
val canInvite: Boolean
|
||||
val isSearchActive: Boolean
|
||||
val eventSink: (InvitePeopleEvents) -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
||||
override val values: Sequence<InvitePeopleState>
|
||||
get() = sequenceOf(
|
||||
PreviewInvitePeopleState(),
|
||||
PreviewInvitePeopleState(canInvite = true),
|
||||
PreviewInvitePeopleState(isSearchActive = true)
|
||||
)
|
||||
}
|
||||
|
||||
private data class PreviewInvitePeopleState(
|
||||
override val canInvite: Boolean = false,
|
||||
override val isSearchActive: Boolean = false,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit = {}
|
||||
) : InvitePeopleState
|
||||
|
|
@ -34,6 +34,7 @@ dependencies {
|
|||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.usersearch.impl)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(projects.services.apperror.api)
|
||||
api(projects.features.invitepeople.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
|
|||
|
|
@ -27,23 +27,30 @@ 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.di.annotations.AppCoroutineScope
|
||||
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.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.services.apperror.api.AppErrorStateService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DefaultInvitePeoplePresenter @AssistedInject constructor(
|
||||
@Assisted private val room: JoinedRoom,
|
||||
private val userRepository: UserRepository,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
@AppCoroutineScope private val coroutineScope: CoroutineScope,
|
||||
private val appErrorStateService: AppErrorStateService,
|
||||
) : InvitePeoplePresenter {
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -97,12 +104,30 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor(
|
|||
searchResults.toggleUser(it.user)
|
||||
}
|
||||
is InvitePeopleEvents.SendInvites -> {
|
||||
coroutineScope.sendInvites(selectedUsers.value)
|
||||
}
|
||||
is InvitePeopleEvents.CloseSearch -> {
|
||||
searchActive = false
|
||||
searchQuery = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.sendInvites(selectedUsers: List<MatrixUser>) = launch {
|
||||
val anyInviteFailed = selectedUsers
|
||||
.map { room.inviteUserById(it.userId) }
|
||||
.any { it.isFailure }
|
||||
|
||||
if (anyInviteFailed) {
|
||||
appErrorStateService.showError(
|
||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("toggleUserInSelectedUsers")
|
||||
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
|
||||
value = if (value.contains(user)) {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ class DefaultInvitePeopleRenderer @Inject constructor() : InvitePeopleRenderer {
|
|||
if (state is DefaultInvitePeopleState) {
|
||||
InvitePeopleView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onSubmitClick = {},
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class DefaultInvitePeopleState(
|
||||
val canInvite: Boolean,
|
||||
override val canInvite: Boolean,
|
||||
val searchQuery: String,
|
||||
val showSearchLoader: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
override val isSearchActive: Boolean,
|
||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||
): InvitePeopleState
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
package io.element.android.features.invitepeople.impl
|
||||
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
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
|
||||
|
|
@ -21,6 +21,7 @@ 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 com.bumble.appyx.core.plugin.BackPressHandler
|
||||
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
|
||||
|
|
@ -28,7 +29,6 @@ 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
|
||||
|
|
@ -46,87 +46,46 @@ 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),
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
InvitePeopleSearchBar(
|
||||
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(
|
||||
private fun InvitePeopleSearchBar(
|
||||
query: String,
|
||||
state: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
showLoader: Boolean,
|
||||
|
|
@ -219,9 +178,5 @@ private fun RoomInviteMembersSearchBar(
|
|||
@Composable
|
||||
internal fun RoomInviteMembersViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =
|
||||
ElementPreview {
|
||||
InvitePeopleView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onSubmitClick = {},
|
||||
)
|
||||
InvitePeopleView(state = state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ dependencies {
|
|||
implementation(projects.features.reportroom.api)
|
||||
implementation(projects.features.roommembermoderation.api)
|
||||
implementation(projects.features.changeroommemberroles.api)
|
||||
implementation(projects.features.invitepeople.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* 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.roomdetails.impl.invite
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface RoomInviteMembersEvents {
|
||||
data class ToggleUser(val user: MatrixUser) : RoomInviteMembersEvents
|
||||
data class UpdateSearchQuery(val query: String) : RoomInviteMembersEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : RoomInviteMembersEvents
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ package io.element.android.features.roomdetails.impl.invite
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
|
|
@ -18,27 +17,22 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
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.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.apperror.api.AppErrorStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class RoomInviteMembersNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
private val room: JoinedRoom,
|
||||
private val presenter: RoomInviteMembersPresenter,
|
||||
private val appErrorStateService: AppErrorStateService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val invitePeopleRenderer: InvitePeopleRenderer,
|
||||
private val invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -48,31 +42,17 @@ class RoomInviteMembersNode @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private val invitePeoplePresenter = invitePeoplePresenterFactory.create(room)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val context = LocalContext.current.applicationContext
|
||||
|
||||
val state = invitePeoplePresenter.present()
|
||||
RoomInviteMembersView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
invitePeopleView = { invitePeopleRenderer.Render(state, Modifier) },
|
||||
onBackClick = { navigateUp() },
|
||||
onSubmitClick = { users ->
|
||||
navigateUp()
|
||||
|
||||
coroutineScope.launch {
|
||||
val anyInviteFailed = users
|
||||
.map { room.inviteUserById(it.userId) }
|
||||
.any { it.isFailure }
|
||||
|
||||
if (anyInviteFailed) {
|
||||
appErrorStateService.showError(
|
||||
title = context.getString(CommonStrings.common_unable_to_invite_title),
|
||||
body = context.getString(CommonStrings.common_unable_to_invite_message),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
onSubmitClick = { navigateUp() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
/*
|
||||
* 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.roomdetails.impl.invite
|
||||
|
||||
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 io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomInviteMembersPresenter @Inject constructor(
|
||||
private val userRepository: UserRepository,
|
||||
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : Presenter<RoomInviteMembersState> {
|
||||
@Composable
|
||||
override fun present(): RoomInviteMembersState {
|
||||
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 RoomInviteMembersState(
|
||||
canInvite = selectedUsers.value.isNotEmpty(),
|
||||
selectedUsers = selectedUsers.value,
|
||||
searchQuery = searchQuery,
|
||||
isSearchActive = searchActive,
|
||||
searchResults = searchResults.value,
|
||||
showSearchLoader = showSearchLoader.value,
|
||||
eventSink = {
|
||||
when (it) {
|
||||
is RoomInviteMembersEvents.OnSearchActiveChanged -> {
|
||||
searchActive = it.active
|
||||
searchQuery = ""
|
||||
}
|
||||
|
||||
is RoomInviteMembersEvents.UpdateSearchQuery -> {
|
||||
searchQuery = it.query
|
||||
}
|
||||
|
||||
is RoomInviteMembersEvents.ToggleUser -> {
|
||||
selectedUsers.toggleUser(it.user)
|
||||
searchResults.toggleUser(it.user)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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 {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
roomMemberListDataSource.search("").toImmutableList()
|
||||
}
|
||||
}.runCatchingUpdatingState(roomMembers)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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.roomdetails.impl.invite
|
||||
|
||||
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 RoomInviteMembersState(
|
||||
val canInvite: Boolean,
|
||||
val searchQuery: String,
|
||||
val showSearchLoader: Boolean,
|
||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val eventSink: (RoomInviteMembersEvents) -> Unit,
|
||||
)
|
||||
|
||||
data class InvitableUser(
|
||||
val matrixUser: MatrixUser,
|
||||
val isSelected: Boolean = false,
|
||||
val isAlreadyJoined: Boolean = false,
|
||||
val isAlreadyInvited: Boolean = false,
|
||||
val isUnresolved: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* 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.roomdetails.impl.invite
|
||||
|
||||
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 RoomInviteMembersStateProvider : PreviewParameterProvider<RoomInviteMembersState> {
|
||||
override val values: Sequence<RoomInviteMembersState>
|
||||
get() = sequenceOf(
|
||||
aRoomInviteMembersState(),
|
||||
aRoomInviteMembersState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query"),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
|
||||
aRoomInviteMembersState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
|
||||
aRoomInviteMembersState(
|
||||
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),
|
||||
)
|
||||
)
|
||||
),
|
||||
aRoomInviteMembersState(
|
||||
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")),
|
||||
)
|
||||
)
|
||||
),
|
||||
aRoomInviteMembersState(
|
||||
isSearchActive = true,
|
||||
canInvite = true,
|
||||
searchQuery = "@alice:server.org",
|
||||
searchResults = SearchBarResultState.Results(
|
||||
persistentListOf(
|
||||
InvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
|
||||
)
|
||||
),
|
||||
showSearchLoader = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aRoomInviteMembersState(
|
||||
canInvite: Boolean = false,
|
||||
searchQuery: String = "",
|
||||
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.Initial(),
|
||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
isSearchActive: Boolean = false,
|
||||
showSearchLoader: Boolean = false,
|
||||
): RoomInviteMembersState {
|
||||
return RoomInviteMembersState(
|
||||
canInvite = canInvite,
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers,
|
||||
isSearchActive = isSearchActive,
|
||||
showSearchLoader = showSearchLoader,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -8,47 +8,35 @@
|
|||
package io.element.android.features.roomdetails.impl.invite
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.features.invitepeople.api.InvitePeopleEvents
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
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 RoomInviteMembersView(
|
||||
state: RoomInviteMembersState,
|
||||
state: InvitePeopleState,
|
||||
invitePeopleView: @Composable () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
onSubmitClick: (List<MatrixUser>) -> Unit,
|
||||
onSubmitClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -57,45 +45,28 @@ fun RoomInviteMembersView(
|
|||
RoomInviteMembersTopBar(
|
||||
onBackClick = {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(false))
|
||||
state.eventSink(InvitePeopleEvents.CloseSearch)
|
||||
} else {
|
||||
onBackClick()
|
||||
}
|
||||
},
|
||||
onSubmitClick = { onSubmitClick(state.selectedUsers) },
|
||||
onSubmitClick = {
|
||||
state.eventSink(InvitePeopleEvents.SendInvites)
|
||||
onSubmitClick()
|
||||
},
|
||||
canSend = state.canInvite,
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
) {
|
||||
RoomInviteMembersSearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
query = state.searchQuery,
|
||||
showLoader = state.showSearchLoader,
|
||||
selectedUsers = state.selectedUsers,
|
||||
state = state.searchResults,
|
||||
active = state.isSearchActive,
|
||||
onActiveChange = { state.eventSink(RoomInviteMembersEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChange = { state.eventSink(RoomInviteMembersEvents.UpdateSearchQuery(it)) },
|
||||
onToggleUser = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) },
|
||||
)
|
||||
|
||||
if (!state.isSearchActive) {
|
||||
SelectedUsersRowList(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemove = { state.eventSink(RoomInviteMembersEvents.ToggleUser(it)) },
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
)
|
||||
}
|
||||
invitePeopleView()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,100 +90,12 @@ private fun RoomInviteMembersTopBar(
|
|||
)
|
||||
}
|
||||
|
||||
@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 -> stringResource(R.string.screen_room_details_already_a_member)
|
||||
invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_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(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = ElementPreview {
|
||||
internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
|
||||
RoomInviteMembersView(
|
||||
state = state,
|
||||
invitePeopleView = {},
|
||||
onBackClick = {},
|
||||
onSubmitClick = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,14 @@
|
|||
|
||||
package io.element.android.services.apperror.api
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface AppErrorStateService {
|
||||
val appErrorStateFlow: StateFlow<AppErrorState>
|
||||
|
||||
fun showError(title: String, body: String)
|
||||
|
||||
fun showError(@StringRes titleRes: Int, @StringRes bodyRes: Int)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
package io.element.android.services.apperror.impl
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.services.apperror.api.AppErrorState
|
||||
import io.element.android.services.apperror.api.AppErrorStateService
|
||||
|
|
@ -18,7 +20,9 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultAppErrorStateService @Inject constructor() : AppErrorStateService {
|
||||
class DefaultAppErrorStateService @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AppErrorStateService {
|
||||
private val currentAppErrorState = MutableStateFlow<AppErrorState>(AppErrorState.NoError)
|
||||
override val appErrorStateFlow: StateFlow<AppErrorState> = currentAppErrorState
|
||||
|
||||
|
|
@ -31,4 +35,10 @@ class DefaultAppErrorStateService @Inject constructor() : AppErrorStateService {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun showError(titleRes: Int, bodyRes: Int) {
|
||||
val title = context.getString(titleRes)
|
||||
val body = context.getString(bodyRes)
|
||||
showError(title, body)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue