Merge pull request #5166 from element-hq/feature/fga/create_room_flow_rework

Create room flow rework
This commit is contained in:
Benoit Marty 2025-08-18 09:38:28 +02:00 committed by GitHub
commit de16268674
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
340 changed files with 2539 additions and 1784 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -11,17 +11,17 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
fun onOpenRoomDirectory()
fun onRoomCreated(roomId: RoomId)
}
}

View file

@ -1,4 +1,3 @@
import extension.ComponentMergingStrategy
import extension.setupAnvil
/*
@ -23,7 +22,7 @@ android {
}
}
setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
setupAnvil()
dependencies {
implementation(projects.libraries.core)
@ -41,6 +40,7 @@ dependencies {
implementation(projects.services.analytics.api)
implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api)
implementation(projects.features.invitepeople.api)
api(projects.features.createroom.api)
testImplementation(libs.test.junit)
@ -56,7 +56,7 @@ dependencies {
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.features.createroom.test)
testImplementation(projects.features.startchat.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)

View file

@ -1,83 +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.createroom.impl
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.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.CreateRoomNavigator
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.di.CreateRoomComponent
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.di.SessionScope
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
class ConfigureRoomFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : DaggerComponentOwner,
BaseFlowNode<ConfigureRoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
private val component by lazy {
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
}
private val navigator = plugins<CreateRoomNavigator>().first()
override val daggerComponent: Any
get() = component
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data object ConfigureRoom : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> {
val callback = object : AddPeopleNode.Callback {
override fun onContinue() {
backstack.push(NavTarget.ConfigureRoom)
}
}
createNode<AddPeopleNode>(buildContext = buildContext, plugins = listOf(callback))
}
NavTarget.ConfigureRoom -> {
createNode<ConfigureRoomNode>(buildContext = buildContext, plugins = listOf(navigator))
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -8,28 +8,25 @@
package io.element.android.features.createroom.impl
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.replace
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.DefaultCreateRoomNavigator
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.createroom.impl.joinbyaddress.JoinRoomByAddressNode
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
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.OverlayView
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@ -38,53 +35,48 @@ class CreateRoomFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<CreateRoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
initialElement = NavTarget.ConfigureRoom,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data object NewRoom : NavTarget
@Parcelize
data object JoinByAddress : NavTarget
private fun onRoomCreated(roomId: RoomId) {
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onRoomCreated(roomId) }
}
private val navigator = DefaultCreateRoomNavigator(
backstack = backstack,
overlay = overlay,
openRoom = { roomIdOrAlias, viaServers ->
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) }
},
openRoomDirectory = {
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoomDirectory() }
}
)
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> {
createNode<CreateRoomRootNode>(buildContext = buildContext, plugins = listOf(navigator))
NavTarget.ConfigureRoom -> {
val callback = object : ConfigureRoomNode.Callback {
override fun onCreateRoomSuccess(roomId: RoomId) {
backstack.replace(NavTarget.AddPeople(roomId))
}
}
createNode<ConfigureRoomNode>(buildContext, plugins = listOf(callback))
}
NavTarget.NewRoom -> {
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(navigator))
}
NavTarget.JoinByAddress -> {
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
is NavTarget.AddPeople -> {
val inputs = AddPeopleNode.Inputs(navTarget.roomId)
val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback {
override fun onFinish() {
onRoomCreated(navTarget.roomId)
}
}
createNode<AddPeopleNode>(buildContext, plugins = listOf(inputs, callback))
}
}
}
@Composable
override fun View(modifier: Modifier) {
Box(modifier = modifier) {
BackstackView()
OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
}
BackstackView()
}
sealed interface NavTarget : Parcelable {
@Parcelize
data object ConfigureRoom : NavTarget
@Parcelize
data class AddPeople(val roomId: RoomId) : NavTarget
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -13,10 +13,10 @@ import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
@ContributesBinding(SessionScope::class)
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.
@ -16,30 +16,46 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.impl.di.CreateRoomScope
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.core.RoomId
@ContributesNode(CreateRoomScope::class)
@ContributesNode(SessionScope::class)
class AddPeopleNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AddPeoplePresenter,
invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
private val invitePeopleRenderer: InvitePeopleRenderer,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val roomId: RoomId,
) : NodeInputs
interface Callback : Plugin {
fun onContinue()
fun onFinish()
}
private fun onContinue() {
plugins<Callback>().forEach { it.onContinue() }
private fun onFinish() {
plugins<Callback>().forEach { it.onFinish() }
}
private val roomId = inputs<Inputs>().roomId
private val invitePeoplePresenter = invitePeoplePresenterFactory.create(
joinedRoom = null,
roomId = roomId,
)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val state = invitePeoplePresenter.present()
AddPeopleView(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
onNextClick = this::onContinue,
)
onFinish = ::onFinish,
) {
invitePeopleRenderer.Render(state, Modifier)
}
}
}

View file

@ -1,37 +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.createroom.impl.addpeople
import androidx.compose.runtime.Composable
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.SelectionMode
import io.element.android.features.createroom.impl.userlist.UserListPresenter
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.usersearch.api.UserRepository
import javax.inject.Inject
class AddPeoplePresenter @Inject constructor(
userListPresenterFactory: UserListPresenter.Factory,
userRepository: UserRepository,
dataStore: CreateRoomDataStore,
) : Presenter<UserListState> {
private val userListPresenter = userListPresenterFactory.create(
UserListPresenterArgs(
selectionMode = SelectionMode.Multiple,
),
userRepository,
dataStore.selectedUserListDataStore,
)
@Composable
override fun present(): UserListState {
return userListPresenter.present()
}
}

View file

@ -1,46 +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.createroom.impl.addpeople
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.createroom.impl.userlist.SelectionMode
import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList
import io.element.android.features.createroom.impl.userlist.aUserListState
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.toImmutableList
open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListState> {
override val values: Sequence<UserListState>
get() = sequenceOf(
aUserListState(),
aUserListState(
searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
selectedUsers = aMatrixUserList().toImmutableList(),
isSearchActive = false,
selectionMode = SelectionMode.Multiple,
),
aUserListState(
searchResults = SearchBarResultState.Results(
aMatrixUserList()
.mapIndexed { index, matrixUser ->
UserSearchResult(matrixUser, index % 2 == 0)
}
.toImmutableList()
),
selectedUsers = aMatrixUserList().toImmutableList(),
isSearchActive = true,
selectionMode = SelectionMode.Multiple,
),
aUserListState(
recentDirectRooms = aRecentDirectRoomList(),
),
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 New Vector Ltd.
* 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.
@ -7,77 +7,68 @@
package io.element.android.features.createroom.impl.addpeople
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.UserListView
import io.element.android.features.createroom.impl.userlist.UserListEvents
import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.libraries.designsystem.components.button.BackButton
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.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Scaffold
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: UserListState,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
state: InvitePeopleState,
onFinish: () -> Unit,
modifier: Modifier = Modifier,
invitePeopleView: @Composable () -> Unit,
) {
Scaffold(
HeaderFooterPage(
modifier = modifier,
contentPadding = PaddingValues(0.dp),
topBar = {
AddPeopleViewTopBar(
hasSelectedUsers = state.selectedUsers.isNotEmpty(),
onBackClick = {
if (state.isSearchActive) {
state.eventSink(UserListEvents.OnSearchActiveChanged(false))
} else {
onBackClick()
}
AddPeopleTopBar(onSkipClick = onFinish)
},
footer = {
Button(
text = stringResource(CommonStrings.action_finish),
onClick = {
state.eventSink(InvitePeopleEvents.SendInvites)
onFinish()
},
onNextClick = onNextClick,
enabled = state.canInvite,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
)
}
) { padding ->
UserListView(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding),
state = state,
showBackButton = false,
onSelectUser = {},
onDeselectUser = {},
)
}
},
content = invitePeopleView
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AddPeopleViewTopBar(
hasSelectedUsers: Boolean,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
private fun AddPeopleTopBar(
onSkipClick: () -> Unit,
) {
TopAppBar(
titleStr = stringResource(id = R.string.screen_create_room_add_people_title),
navigationIcon = { BackButton(onClick = onBackClick) },
titleStr = stringResource(R.string.screen_create_room_add_people_title),
actions = {
val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
TextButton(
text = stringResource(id = textActionResId),
onClick = onNextClick,
text = stringResource(CommonStrings.action_skip),
onClick = onSkipClick,
)
}
)
@ -85,10 +76,10 @@ private fun AddPeopleViewTopBar(
@PreviewsDayNight
@Composable
internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
AddPeopleView(
state = state,
onBackClick = {},
onNextClick = {},
invitePeopleView = {},
onFinish = {},
)
}

View file

@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents {
@ -16,7 +15,6 @@ sealed interface ConfigureRoomEvents {
data class RoomVisibilityChanged(val visibilityItem: RoomVisibilityItem) : ConfigureRoomEvents
data class RoomAccessChanged(val roomAccess: RoomAccessItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
data class RemoveUserFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
data object CreateRoom : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents

View file

@ -18,19 +18,20 @@ 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.createroom.CreateRoomNavigator
import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(CreateRoomScope::class)
@ContributesNode(SessionScope::class)
class ConfigureRoomNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: ConfigureRoomPresenter,
private val analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
private val navigator = plugins<CreateRoomNavigator>().first()
interface Callback : Plugin {
fun onCreateRoomSuccess(roomId: RoomId)
}
init {
lifecycle.subscribe(
@ -40,6 +41,10 @@ class ConfigureRoomNode @AssistedInject constructor(
)
}
private fun onCreateRoomSuccess(roomId: RoomId) {
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -47,9 +52,7 @@ class ConfigureRoomNode @AssistedInject constructor(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
onCreateRoomSuccess = {
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
},
onCreateRoomSuccess = ::onCreateRoomSuccess,
)
}
}

View file

@ -18,8 +18,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -50,7 +48,7 @@ import javax.inject.Inject
import kotlin.jvm.optionals.getOrDefault
class ConfigureRoomPresenter @Inject constructor(
private val dataStore: CreateRoomDataStore,
private val dataStore: CreateRoomConfigStore,
private val matrixClient: MatrixClient,
private val mediaPickerProvider: PickerProvider,
private val mediaPreProcessor: MediaPreProcessor,
@ -66,7 +64,7 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable
override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
@ -121,7 +119,6 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.RoomVisibilityChanged -> dataStore.setRoomVisibility(event.visibilityItem)
is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)

View file

@ -7,7 +7,6 @@
package io.element.android.features.createroom.impl.configureroom
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction

View file

@ -8,7 +8,6 @@
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.aMatrixUserList

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
@ -58,7 +57,6 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.matrix.ui.room.address.RoomAddressField
import io.element.android.libraries.permissions.api.PermissionsView
@ -112,16 +110,6 @@ fun ConfigureRoomView(
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
)
if (state.config.invites.isNotEmpty()) {
SelectedUsersRowList(
contentPadding = PaddingValues(horizontal = 24.dp),
selectedUsers = state.config.invites,
onUserRemove = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(it))
},
)
}
RoomVisibilityOptions(
selected = when (state.config.roomVisibility) {
is RoomVisibilityState.Private -> RoomVisibilityItem.Private

View file

@ -5,10 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl
package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

View file

@ -5,45 +5,29 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl
package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomAccess
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
import java.io.File
import javax.inject.Inject
@SingleIn(CreateRoomScope::class)
class CreateRoomDataStore @Inject constructor(
val selectedUserListDataStore: UserListDataStore,
class CreateRoomConfigStore @Inject constructor(
private val roomAliasHelper: RoomAliasHelper,
) {
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
private var cachedAvatarUri: Uri? = null
set(value) {
field?.path?.let { File(it) }?.safeDelete()
field = value
}
val createRoomConfigWithInvites: Flow<CreateRoomConfig> = combine(
selectedUserListDataStore.selectedUsers,
createRoomConfigFlow,
) { selectedUsers, config ->
config.copy(invites = selectedUsers.toImmutableList())
}
fun getCreateRoomConfigFlow(): StateFlow<CreateRoomConfig> = createRoomConfigFlow
fun setRoomName(roomName: String) {
createRoomConfigFlow.getAndUpdate { config ->

View file

@ -1,28 +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.createroom.impl.di
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.MergeSubcomponent
import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
@SingleIn(CreateRoomScope::class)
@MergeSubcomponent(CreateRoomScope::class)
interface CreateRoomComponent : NodeFactoriesBindings {
@MergeSubcomponent.Builder
interface Builder {
fun build(): CreateRoomComponent
}
@ContributesTo(SessionScope::class)
interface ParentBindings {
fun createRoomComponentBuilder(): Builder
}
}

View file

@ -1,10 +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.createroom.impl.di
abstract class CreateRoomScope private constructor()

View file

@ -1,15 +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.createroom.impl.root
import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface CreateRoomRootEvents {
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
data object CancelStartDM : CreateRoomRootEvents
}

View file

@ -14,6 +14,4 @@
<string name="screen_create_room_room_name_label">"Назва пакоя"</string>
<string name="screen_create_room_title">"Стварыце пакой"</string>
<string name="screen_create_room_topic_label">"Тэма (неабавязкова)"</string>
<string name="screen_room_directory_search_title">"Каталог пакояў"</string>
<string name="screen_start_chat_error_starting_chat">"Пры спробе пачаць чат адбылася памылка"</string>
</resources>

View file

@ -15,9 +15,4 @@
<string name="screen_create_room_room_visibility_section_title">"Видимост на стаята"</string>
<string name="screen_create_room_title">"Създаване на стая"</string>
<string name="screen_create_room_topic_label">"Тема за разговор (незадължително)"</string>
<string name="screen_start_chat_join_room_by_address_action">"Присъединяване към стая по адрес"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Не е валиден адрес"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Въведете…"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Стаята не е намерена"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"напр. #room-name:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_room_visibility_section_title">"Viditelnost místnosti"</string>
<string name="screen_create_room_title">"Vytvořit místnost"</string>
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
<string name="screen_room_directory_search_title">"Adresář místností"</string>
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
<string name="screen_start_chat_join_room_by_address_action">"Vstoupit do místnosti pomocí adresy"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Neplatná adresa"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Zadejte…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Odpovídající místnost nalezena"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Místnost nebyla nalezena"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"např. #room-name:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Gallwch newid hyn unrhyw bryd yng ngosodiadau ystafell."</string>
<string name="screen_create_room_room_visibility_section_title">"Gwelededd yr ystafell"</string>
<string name="screen_create_room_title">"Creu ystafell"</string>
<string name="screen_create_room_topic_label">"Pwnc (dewisol)"</string>
<string name="screen_room_directory_search_title">"Cyfeiriadur ystafelloedd"</string>
<string name="screen_start_chat_error_starting_chat">"Digwyddodd gwall wrth geisio cychwyn sgwrs"</string>
<string name="screen_start_chat_join_room_by_address_action">"Ymuno â\'r ystafell yn ôl cyfeiriad"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ddim yn gyfeiriad dilys"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Ewch i mewn…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Cafwyd hyd i ystafell gyfatebol"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Heb ganfod yr ystafell"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"e.e. #enw-ystafell:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Du kan ændre dette når som helst i rummets indstillinger."</string>
<string name="screen_create_room_room_visibility_section_title">"Rummets synlighed"</string>
<string name="screen_create_room_title">"Opret et rum"</string>
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
<string name="screen_room_directory_search_title">"Register over rum"</string>
<string name="screen_start_chat_error_starting_chat">"Der opstod en fejl under forsøget på at starte en samtale"</string>
<string name="screen_start_chat_join_room_by_address_action">"Tilslut dig rummet med adressen"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ikke en gyldig adresse"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Indtast…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Matchende rum fundet"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Rum ikke fundet"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"f.eks. #rummets-navn:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Sie können dies aber jederzeit in den Chatroomeinstellungen ändern."</string>
<string name="screen_create_room_room_visibility_section_title">" Sichtbarkeit des Chatrooms"</string>
<string name="screen_create_room_title">"Raum erstellen"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
<string name="screen_room_directory_search_title">"Raum-Verzeichnis"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
<string name="screen_start_chat_join_room_by_address_action">"Raum per Adresse betreten"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Keine gültige Adresse"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Eintreten…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Passender Raum gefunden"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Raum nicht gefunden"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"z. B. #room -name:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα αίθουσας"</string>
<string name="screen_create_room_title">"Δημιουργία αίθουσας"</string>
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
<string name="screen_room_directory_search_title">"Κατάλογος αιθουσών"</string>
<string name="screen_start_chat_error_starting_chat">"Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"</string>
<string name="screen_start_chat_join_room_by_address_action">"Συμμετοχή σε αίθουσα μέσω διεύθυνσης"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Μη έγκυρη διεύθυνση"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Εισάγετε…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Βρέθηκε η αντίστοιχη αίθουσα"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Η αίθουσα δεν βρέθηκε"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"π.χ. #όνομα-αίθουσας:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Puedes cambiar esto en cualquier momento en los ajustes de la sala."</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidad de la sala"</string>
<string name="screen_create_room_title">"Crear una sala"</string>
<string name="screen_create_room_topic_label">"Tema (opcional)"</string>
<string name="screen_room_directory_search_title">"Directorio de salas"</string>
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Unirse a una sala por su dirección"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Dirección no válida"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Introducir…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Sala encontrada"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"No se encontró la sala"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"p. ej., #nombre-de-la-sala:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_room_visibility_section_title">"Jututoa nähtavus"</string>
<string name="screen_create_room_title">"Loo jututuba"</string>
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
<string name="screen_room_directory_search_title">"Jututubade kataloog"</string>
<string name="screen_start_chat_error_starting_chat">"Vestluse alustamisel tekkis viga"</string>
<string name="screen_start_chat_join_room_by_address_action">"Liitu jututoaga aadressi alusel"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"See pole kehtiv aadress"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Sisene…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Leidsime vastava jututoa"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Jututuba ei leidu"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"nt. #jututoa-nimi:matrix.org"</string>
</resources>

View file

@ -16,8 +16,4 @@ Gelaren ezarpenetan aldatu dezakezu hobespena."</string>
<string name="screen_create_room_room_visibility_section_title">"Gelaren ikusgarritasuna"</string>
<string name="screen_create_room_title">"Sortu gela"</string>
<string name="screen_create_room_topic_label">"Mintzagaia (aukerakoa)"</string>
<string name="screen_room_directory_search_title">"Gelen direktorioa"</string>
<string name="screen_start_chat_error_starting_chat">"Errorea gertatu da txata hasten saiatzean"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Sartu…"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Ez da gela aurkitu"</string>
</resources>

View file

@ -17,12 +17,4 @@
<string name="screen_create_room_room_visibility_section_title">"نمایانی اتاق"</string>
<string name="screen_create_room_title">"ایجاد اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_room_directory_search_title">"فهرست اتاق‌ها"</string>
<string name="screen_start_chat_error_starting_chat">"هنگام تلاش برای شروع چت خطایی روی داد"</string>
<string name="screen_start_chat_join_room_by_address_action">"پیوستن به اتاق با نشانی"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"نشانی معتبری نیست"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"ورود…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"اتاق مطابق پیدا شد"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"اتاق پیدا نشد"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"نمونه: #room-name:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Voit muuttaa tämän milloin tahansa huoneen asetuksista."</string>
<string name="screen_create_room_room_visibility_section_title">"Huoneen näkyvyys"</string>
<string name="screen_create_room_title">"Luo huone"</string>
<string name="screen_create_room_topic_label">"Aihe (valinnainen)"</string>
<string name="screen_room_directory_search_title">"Huoneluettelo"</string>
<string name="screen_start_chat_error_starting_chat">"Keskustelun aloituksessa tapahtui virhe"</string>
<string name="screen_start_chat_join_room_by_address_action">"Liity huoneeseen osoitteella"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Osoite ei ole kelvollinen"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Syötä…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Täsmäävä huone löytyi"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Huonetta ei löytynyt"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"esim. #huoneen-nimi:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
<string name="screen_create_room_room_visibility_section_title">"Visibilité du salon"</string>
<string name="screen_create_room_title">"Créer un salon"</string>
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
<string name="screen_room_directory_search_title">"Annuaire des salons"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur sest produite lors de la tentative de création de la discussion"</string>
<string name="screen_start_chat_join_room_by_address_action">"Saisir une adresse de salon"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ce nest pas une adresse valide"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Saisir…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Ce salon existe"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Salon non trouvé"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"ex: #nom-du-salon:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Ezt bármikor módosíthatja a szobabeállításokban."</string>
<string name="screen_create_room_room_visibility_section_title">"Szoba láthatósága"</string>
<string name="screen_create_room_title">"Szoba létrehozása"</string>
<string name="screen_create_room_topic_label">"Téma (nem kötelező)"</string>
<string name="screen_room_directory_search_title">"Szobakatalógus"</string>
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string>
<string name="screen_start_chat_join_room_by_address_action">"Csatlakozás a szobához cím szerint"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Nem érvényes cím"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Írja be…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Megfelelő szoba található"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Szoba nem található"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"pl. #szoba-neve:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
<string name="screen_create_room_title">"Buat ruangan"</string>
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
<string name="screen_room_directory_search_title">"Direktori ruangan"</string>
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
<string name="screen_start_chat_join_room_by_address_action">"Bergabung dalam ruangan berdasarkan alamat"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Bukan alamat yang valid"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Masuk…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Ruangan yang cocok ditemukan"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Ruangan tidak ditemukan"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"mis. #nama-ruangan:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilità della stanza"</string>
<string name="screen_create_room_title">"Crea una stanza"</string>
<string name="screen_create_room_topic_label">"Argomento (facoltativo)"</string>
<string name="screen_room_directory_search_title">"Elenco delle stanze"</string>
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Accedi alla stanza tramite indirizzo"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Indirizzo non valido"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Inserisci…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Stanza trovata"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Stanza non trovata"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"ad esempio #room -name:matrix.org"</string>
</resources>

View file

@ -10,6 +10,4 @@
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
<string name="screen_create_room_title">"ოთახის შექმნა"</string>
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
<string name="screen_room_directory_search_title">"ოთახის კატალოგი"</string>
<string name="screen_start_chat_error_starting_chat">"ჩატის დაწყების მცდელობისას შეცდომა მოხდა"</string>
</resources>

View file

@ -10,5 +10,4 @@ Tai galite bet kada pakeisti kambario nustatymuose."</string>
<string name="screen_create_room_room_name_label">"Kambario pavadinimas"</string>
<string name="screen_create_room_title">"Kurti kambarį"</string>
<string name="screen_create_room_topic_label">"Tema (nebūtina)"</string>
<string name="screen_start_chat_error_starting_chat">"Bandant pradėti pokalbį įvyko klaida"</string>
</resources>

View file

@ -19,12 +19,4 @@ Du kan endre dette når som helst i rominnstillingene."</string>
<string name="screen_create_room_room_visibility_section_title">"Romsynlighet"</string>
<string name="screen_create_room_title">"Opprett et rom"</string>
<string name="screen_create_room_topic_label">"Emne (valgfritt)"</string>
<string name="screen_room_directory_search_title">"Romkatalog"</string>
<string name="screen_start_chat_error_starting_chat">"Det oppstod en feil når du prøvde å starte en chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Bli med i rommet med adresse"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ikke en gyldig adresse"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Gå inn…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Matchende rom funnet"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Rom ikke funnet"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"f.eks. #rom-navn:matrix.org"</string>
</resources>

View file

@ -16,6 +16,4 @@ Je kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."</string>
<string name="screen_create_room_room_name_label">"Naam van de kamer"</string>
<string name="screen_create_room_title">"Creëer een kamer"</string>
<string name="screen_create_room_topic_label">"Onderwerp (optioneel)"</string>
<string name="screen_room_directory_search_title">"Kamergids"</string>
<string name="screen_start_chat_error_starting_chat">"Er is een fout opgetreden bij het starten van een chat"</string>
</resources>

View file

@ -19,12 +19,4 @@ Możesz to zmienić w ustawieniach pokoju."</string>
<string name="screen_create_room_room_visibility_section_title">"Widoczność pomieszczenia"</string>
<string name="screen_create_room_title">"Utwórz pokój"</string>
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
<string name="screen_room_directory_search_title">"Katalog pokoi"</string>
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
<string name="screen_start_chat_join_room_by_address_action">"Dołącz do pokoju za pomocą adresu"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Nieprawidłowy adres"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Wprowadź…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Znaleziono pasujący pokój"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Nie znaleziono pokoju"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"np. #room-name:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Você pode mudar isso a qualquer momento nas configurações da sala."</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Entrar na sala pelo endereço"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Não é um endereço válido"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Entrar…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Foi encontrada uma sala correspondente"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Sala não encontrada"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"Por exemplo, #nome-da-sala:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Pode alterar esta opção nas definições da sala."</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilidade da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
<string name="screen_room_directory_search_title">"Diretório de salas"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
<string name="screen_start_chat_join_room_by_address_action">"Entrar na sala pelo endereço"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Não é um endereço válido"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Entrar…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Sala correspondente encontrado"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Sala não encontrada"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"por exemplo, #sala:matrix.org"</string>
</resources>

View file

@ -17,6 +17,4 @@ Puteți modifica acest lucru oricând în setări."</string>
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
<string name="screen_create_room_title">"Creați o cameră"</string>
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
<string name="screen_room_directory_search_title">"Director de camere"</string>
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
</resources>

View file

@ -19,12 +19,4 @@
<string name="screen_create_room_room_visibility_section_title">"Видимость комнаты"</string>
<string name="screen_create_room_title">"Создать комнату"</string>
<string name="screen_create_room_topic_label">"Тема (необязательно)"</string>
<string name="screen_room_directory_search_title">"Каталог комнат"</string>
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при запуске чата"</string>
<string name="screen_start_chat_join_room_by_address_action">"Присоединиться к комнате по адресу"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Недействительный адрес"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Ввести…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Соответствующая комната найдена"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Комната не найдена"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"прим. #room-name:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
<string name="screen_create_room_room_visibility_section_title">"Viditeľnosť miestnosti"</string>
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
<string name="screen_room_directory_search_title">"Adresár miestností"</string>
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
<string name="screen_start_chat_join_room_by_address_action">"Pripojte sa do miestnosti podľa adresy"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Neplatná adresa"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Zadajte…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Nájdená zodpovedajúca miestnosť"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Miestnosť sa nenašla"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"napr. #nazov-miestnosti:matrix.org"</string>
</resources>

View file

@ -19,12 +19,4 @@ Du kan ändra detta när som helst i rumsinställningarna."</string>
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
<string name="screen_create_room_title">"Skapa ett rum"</string>
<string name="screen_create_room_topic_label">"Ämne (valfritt)"</string>
<string name="screen_room_directory_search_title">"Rumskatalog"</string>
<string name="screen_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string>
<string name="screen_start_chat_join_room_by_address_action">"Gå med i rum med adress"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Inte en giltig adress"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Ange …"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Matchande rum hittades"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Rummet hittades inte"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"t.ex. #rumsnamn:matrix.org"</string>
</resources>

View file

@ -19,6 +19,4 @@ Bunu istediğiniz zaman oda ayarlarından değiştirebilirsiniz."</string>
<string name="screen_create_room_room_visibility_section_title">"Oda görünürlüğü"</string>
<string name="screen_create_room_title">"Bir oda oluştur"</string>
<string name="screen_create_room_topic_label">"Konu (isteğe bağlı)"</string>
<string name="screen_room_directory_search_title">"Oda dizini"</string>
<string name="screen_start_chat_error_starting_chat">"Sohbet başlatmaya çalışırken bir hata oluştu"</string>
</resources>

View file

@ -19,12 +19,4 @@
<string name="screen_create_room_room_visibility_section_title">"Видимість кімнати"</string>
<string name="screen_create_room_title">"Створити кімнату"</string>
<string name="screen_create_room_topic_label">"Тема (необов\'язково)"</string>
<string name="screen_room_directory_search_title">"Каталог кімнат"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати бесіду сталася помилка"</string>
<string name="screen_start_chat_join_room_by_address_action">"Приєднатися до кімнати за адресою"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Недійсна адреса"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Введіть…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Знайдено відповідну кімнату"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Кімната не знайдена"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"наприклад, #room-name:matrix.org"</string>
</resources>

View file

@ -11,6 +11,4 @@
<string name="screen_create_room_room_name_label">"کمرے کا نام"</string>
<string name="screen_create_room_title">"ایک کمرہ بنائیں"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_room_directory_search_title">"کمرے کا راہنامچہ"</string>
<string name="screen_start_chat_error_starting_chat">"گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"</string>
</resources>

View file

@ -9,5 +9,4 @@
<string name="screen_create_room_room_name_label">"Xona nomi"</string>
<string name="screen_create_room_title">"Xonani yaratish"</string>
<string name="screen_create_room_topic_label">"Mavzu (ixtiyoriy)"</string>
<string name="screen_start_chat_error_starting_chat">"Suhbatni boshlashda xatolik yuz berdi"</string>
</resources>

View file

@ -19,12 +19,4 @@
<string name="screen_create_room_room_visibility_section_title">"聊天室能見度"</string>
<string name="screen_create_room_title">"建立聊天室"</string>
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
<string name="screen_room_directory_search_title">"聊天室目錄"</string>
<string name="screen_start_chat_error_starting_chat">"嘗試開始聊天時發生錯誤"</string>
<string name="screen_start_chat_join_room_by_address_action">"按地址加入聊天室"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"不是有效的位址"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"輸入……"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"找到相符的聊天室"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"找不到聊天室"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"例如 #room-name:matrix.org"</string>
</resources>

View file

@ -19,6 +19,4 @@
<string name="screen_create_room_room_visibility_section_title">"房间可见性"</string>
<string name="screen_create_room_title">"创建聊天室"</string>
<string name="screen_create_room_topic_label">"主题(可选)"</string>
<string name="screen_room_directory_search_title">"聊天室目录"</string>
<string name="screen_start_chat_error_starting_chat">"在开始聊天时发生了错误"</string>
</resources>

View file

@ -19,12 +19,4 @@ You can change this anytime in room settings."</string>
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
<string name="screen_create_room_title">"Create a room"</string>
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
<string name="screen_room_directory_search_title">"Room directory"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Join room by address"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Not a valid address"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Enter…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Matching room found"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Room not found"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"e.g. #room-name:matrix.org"</string>
</resources>

View file

@ -1,50 +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.createroom.impl.addpeople
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class AddPeoplePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private lateinit var presenter: AddPeoplePresenter
@Before
fun setup() {
presenter = AddPeoplePresenter(
FakeUserListPresenterFactory(),
FakeUserRepository(),
CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
)
}
@Test
fun `present - initial state`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// TODO This doesn't actually test anything...
val initialState = awaitItem()
assertThat(initialState)
}
}
}

View file

@ -1,89 +0,0 @@
/*
* Copyright 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.createroom.impl.addpeople
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.createroom.impl.userlist.UserListEvents
import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.features.createroom.impl.userlist.aUserListState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AddPeopleViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invokes the expected callback`() {
val eventsRecorder = EventsRecorder<UserListEvents>()
ensureCalledOnce {
rule.setAddPeopleView(
aUserListState(
eventSink = eventsRecorder,
),
onBackClick = it
)
rule.pressBack()
}
eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
}
@Test
fun `clicking on back during search emits the expected Event`() {
val eventsRecorder = EventsRecorder<UserListEvents>()
rule.setAddPeopleView(
aUserListState(
isSearchActive = true,
eventSink = eventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertSingle(UserListEvents.OnSearchActiveChanged(false))
}
@Test
fun `clicking on skip invokes the expected callback`() {
val eventsRecorder = EventsRecorder<UserListEvents>()
ensureCalledOnce {
rule.setAddPeopleView(
aUserListState(
eventSink = eventsRecorder,
),
onNextClick = it
)
rule.clickOn(CommonStrings.action_skip)
}
eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddPeopleView(
state: UserListState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onNextClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
AddPeopleView(
state = state,
onBackClick = onBackClick,
onNextClick = onNextClick,
)
}
}

View file

@ -5,15 +5,21 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
package io.element.android.features.startchat.impl.configureroom
import android.net.Uri
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfigStore
import io.element.android.features.createroom.impl.configureroom.RoomAccess
import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@ -28,7 +34,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
import io.element.android.libraries.mediapickers.api.PickerProvider
@ -48,8 +53,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@ -67,7 +70,7 @@ private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
@RunWith(RobolectricTestRunner::class)
class ConfigureBaseRoomPresenterTest {
class ConfigureRoomPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@ -124,12 +127,11 @@ class ConfigureBaseRoomPresenterTest {
@Test
fun `present - state is updated when fields are changed`() = runTest {
val userListDataStore = UserListDataStore()
val pickerProvider = FakePickerProvider()
val permissionsPresenter = FakePermissionsPresenter()
val roomAliasHelper = FakeRoomAliasHelper()
val presenter = createConfigureRoomPresenter(
createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
dataStore = CreateRoomConfigStore(roomAliasHelper),
pickerProvider = pickerProvider,
permissionsPresenter = permissionsPresenter,
)
@ -137,20 +139,9 @@ class ConfigureBaseRoomPresenterTest {
val initialState = initialState()
var expectedConfig = CreateRoomConfig()
assertThat(initialState.config).isEqualTo(expectedConfig)
// Select User
val selectedUser1 = aMatrixUser()
val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob")
userListDataStore.selectUser(selectedUser1)
skipItems(1)
userListDataStore.selectUser(selectedUser2)
var newState = awaitItem()
expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2))
assertThat(newState.config).isEqualTo(expectedConfig)
// Room name
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
newState = awaitItem()
var newState = awaitItem()
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(expectedConfig)
@ -206,12 +197,6 @@ class ConfigureBaseRoomPresenterTest {
)
)
assertThat(newState.config).isEqualTo(expectedConfig)
// Remove user
newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1))
newState = awaitItem()
expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
assertThat(newState.config).isEqualTo(expectedConfig)
}
}
@ -263,16 +248,16 @@ class ConfigureBaseRoomPresenterTest {
val matrixClient = createMatrixClient()
val analyticsService = FakeAnalyticsService()
val mediaPreProcessor = FakeMediaPreProcessor()
val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
val dataStore = CreateRoomConfigStore(FakeRoomAliasHelper())
val presenter = createConfigureRoomPresenter(
createRoomDataStore = createRoomDataStore,
dataStore = dataStore,
mediaPreProcessor = mediaPreProcessor,
matrixClient = matrixClient,
analyticsService = analyticsService
)
presenter.test {
val initialState = initialState()
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
dataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
skipItems(1)
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
@ -405,7 +390,7 @@ class ConfigureBaseRoomPresenterTest {
private fun createConfigureRoomPresenter(
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
matrixClient: MatrixClient = createMatrixClient(),
pickerProvider: PickerProvider = FakePickerProvider(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
@ -414,7 +399,7 @@ class ConfigureBaseRoomPresenterTest {
isKnockFeatureEnabled: Boolean = true,
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
) = ConfigureRoomPresenter(
dataStore = createRoomDataStore,
dataStore = dataStore,
matrixClient = matrixClient,
mediaPickerProvider = pickerProvider,
mediaPreProcessor = mediaPreProcessor,

View file

@ -22,7 +22,7 @@ interface HomeEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onRoomClick(roomId: RoomId)
fun onCreateRoomClick()
fun onStartChatClick()
fun onSettingsClick()
fun onSetUpRecoveryClick()
fun onSessionConfirmRecoveryKeyClick()

View file

@ -120,8 +120,8 @@ class HomeFlowNode @AssistedInject constructor(
plugins<HomeEntryPoint.Callback>().forEach { it.onSettingsClick() }
}
private fun onCreateRoomClick() {
plugins<HomeEntryPoint.Callback>().forEach { it.onCreateRoomClick() }
private fun onStartChatClick() {
plugins<HomeEntryPoint.Callback>().forEach { it.onStartChatClick() }
}
private fun onSetUpRecoveryClick() {
@ -171,7 +171,7 @@ class HomeFlowNode @AssistedInject constructor(
homeState = state,
onRoomClick = this::onRoomClick,
onSettingsClick = this::onOpenSettings,
onCreateRoomClick = this::onCreateRoomClick,
onStartChatClick = this::onStartChatClick,
onSetUpRecoveryClick = this::onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick,
onRoomSettingsClick = this::onRoomSettingsClick,

View file

@ -72,7 +72,7 @@ fun HomeView(
onSettingsClick: () -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onCreateRoomClick: () -> Unit,
onStartChatClick: () -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onReportRoomClick: (roomId: RoomId) -> Unit,
@ -116,7 +116,7 @@ fun HomeView(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
onCreateRoomClick = { if (firstThrottler.canHandle()) onCreateRoomClick() },
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onMenuActionClick = onMenuActionClick,
modifier = Modifier.padding(top = topPadding),
)
@ -145,7 +145,7 @@ private fun HomeScaffold(
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomId) -> Unit,
onOpenSettings: () -> Unit,
onCreateRoomClick: () -> Unit,
onStartChatClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
modifier: Modifier = Modifier,
) {
@ -236,7 +236,7 @@ private fun HomeScaffold(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onCreateRoomClick,
onCreateRoomClick = onStartChatClick,
contentPadding = PaddingValues(
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
// and include provided bottom padding
@ -280,7 +280,7 @@ private fun HomeScaffold(
floatingActionButton = {
if (state.displayActions) {
FloatingActionButton(
onClick = onCreateRoomClick,
onClick = onStartChatClick,
) {
Icon(
imageVector = CompoundIcons.Plus(),
@ -304,7 +304,7 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onCreateRoomClick = {},
onStartChatClick = {},
onRoomSettingsClick = {},
onReportRoomClick = {},
onMenuActionClick = {},

View file

@ -33,6 +33,7 @@ For now, you can deselect filters in order to see your other chats"</string>
<string name="screen_roomlist_filter_invites">"Invites"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"You don\'t have any pending invites."</string>
<string name="screen_roomlist_filter_low_priority">"Low Priority"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"You dont have any low priority chats yet"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"You can deselect filters in order to see your other chats"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"You dont have chats for this selection"</string>
<string name="screen_roomlist_filter_people">"People"</string>

View file

@ -284,7 +284,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onSettingsClick = onSettingsClick,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onCreateRoomClick = onCreateRoomClick,
onStartChatClick = onCreateRoomClick,
onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick,
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.invitepeople.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

View file

@ -0,0 +1,13 @@
/*
* 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
data object CloseSearch : InvitePeopleEvents
}

View file

@ -0,0 +1,21 @@
/*
* 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 io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
interface InvitePeoplePresenter : Presenter<InvitePeopleState> {
interface Factory {
fun create(
joinedRoom: JoinedRoom?,
roomId: RoomId,
): InvitePeoplePresenter
}
}

View file

@ -0,0 +1,19 @@
/*
* 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.runtime.Composable
import androidx.compose.ui.Modifier
interface InvitePeopleRenderer {
@Composable
fun Render(
state: InvitePeopleState,
modifier: Modifier,
)
}

View file

@ -0,0 +1,14 @@
/*
* 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 InvitePeopleState {
val canInvite: Boolean
val isSearchActive: Boolean
val eventSink: (InvitePeopleEvents) -> Unit
}

View file

@ -0,0 +1,35 @@
/*
* 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(
aPreviewInvitePeopleState(),
aPreviewInvitePeopleState(canInvite = true),
aPreviewInvitePeopleState(isSearchActive = true)
)
}
private data class PreviewInvitePeopleState(
override val canInvite: Boolean,
override val isSearchActive: Boolean,
override val eventSink: (InvitePeopleEvents) -> Unit,
) : InvitePeopleState
private fun aPreviewInvitePeopleState(
canInvite: Boolean = false,
isSearchActive: Boolean = false,
eventSink: (InvitePeopleEvents) -> Unit = {},
) = PreviewInvitePeopleState(
canInvite = canInvite,
isSearchActive = isSearchActive,
eventSink = eventSink
)

View file

@ -0,0 +1,53 @@
import extension.setupAnvil
/*
* Copyright 2022-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.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.invitepeople.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupAnvil()
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
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)
testImplementation(libs.test.mockk)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}

View file

@ -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
}

View file

@ -5,50 +5,88 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomdetails.impl.invite
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.produceState
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 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.Presenter
import io.element.android.libraries.architecture.map
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.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
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
import javax.inject.Inject
class RoomInviteMembersPresenter @Inject constructor(
class DefaultInvitePeoplePresenter @AssistedInject constructor(
@Assisted private val joinedRoom: JoinedRoom?,
@Assisted private val roomId: RoomId,
private val userRepository: UserRepository,
private val roomMemberListDataSource: RoomMemberListDataSource,
private val coroutineDispatchers: CoroutineDispatchers,
) : Presenter<RoomInviteMembersState> {
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val appErrorStateService: AppErrorStateService,
private val matrixClient: MatrixClient,
) : InvitePeoplePresenter {
@AssistedFactory
@ContributesBinding(SessionScope::class)
interface Factory : InvitePeoplePresenter.Factory {
override fun create(joinedRoom: JoinedRoom?, roomId: RoomId): DefaultInvitePeoplePresenter
}
@Composable
override fun present(): RoomInviteMembersState {
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) }
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
if (joinedRoom == null) {
val result = matrixClient.getJoinedRoom(roomId)
value = if (result == null) {
AsyncData.Failure(Exception("Room not found"))
} else {
AsyncData.Success(result)
}
}
}
LaunchedEffect(Unit) {
fetchMembers(roomMembers)
LaunchedEffect(room.isSuccess()) {
room.dataOrNull()?.let {
fetchMembers(it, roomMembers)
}
}
LaunchedEffect(searchQuery, roomMembers) {
performSearch(
@ -60,33 +98,61 @@ class RoomInviteMembersPresenter @Inject constructor(
)
}
return RoomInviteMembersState(
fun handleEvents(event: InvitePeopleEvents) {
when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
searchActive = event.active
searchQuery = ""
}
is DefaultInvitePeopleEvents.UpdateSearchQuery -> {
searchQuery = event.query
}
is DefaultInvitePeopleEvents.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
}
is InvitePeopleEvents.SendInvites -> {
room.dataOrNull()?.let {
sessionCoroutineScope.sendInvites(it, selectedUsers.value)
}
}
is InvitePeopleEvents.CloseSearch -> {
searchActive = false
searchQuery = ""
}
}
}
return DefaultInvitePeopleState(
room = room.map { },
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)
}
}
}
eventSink = ::handleEvents,
)
}
private fun CoroutineScope.sendInvites(
room: JoinedRoom,
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)) {
@ -134,7 +200,7 @@ class RoomInviteMembersPresenter @Inject constructor(
val isInvited = existingMembership == RoomMembershipState.INVITE
InvitableUser(
matrixUser = result.matrixUser,
isSelected = selectedUsers.value.contains(result.matrixUser) || isJoined || isInvited,
isSelected = selectedUsers.value.contains(result.matrixUser),
isAlreadyJoined = isJoined,
isAlreadyInvited = isInvited,
isUnresolved = result.isUnresolved,
@ -144,11 +210,12 @@ class RoomInviteMembersPresenter @Inject constructor(
}.launchIn(this)
}
private suspend fun fetchMembers(roomMembers: MutableState<AsyncData<ImmutableList<RoomMember>>>) {
private suspend fun fetchMembers(
room: JoinedRoom,
roomMembers: MutableState<AsyncData<ImmutableList<RoomMember>>>
) {
suspend {
withContext(coroutineDispatchers.io) {
roomMemberListDataSource.search("").toImmutableList()
}
room.filterMembers("", coroutineDispatchers.io).toImmutableList()
}.runCatchingUpdatingState(roomMembers)
}
}

View file

@ -0,0 +1,31 @@
/*
* 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,
modifier = modifier
)
} else {
error("Unsupported state type: ${state::javaClass}")
}
}
}

View file

@ -1,30 +1,26 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* 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.roomdetails.impl.invite
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.architecture.AsyncData
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,
data class DefaultInvitePeopleState(
val room: AsyncData<Unit>,
override 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,
)
override val isSearchActive: Boolean,
override val eventSink: (InvitePeopleEvents) -> Unit
) : InvitePeopleState

View file

@ -5,9 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomdetails.impl.invite
package io.element.android.features.invitepeople.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
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
@ -16,15 +17,15 @@ 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>
internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<DefaultInvitePeopleState> {
override val values: Sequence<DefaultInvitePeopleState>
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(
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",
@ -33,15 +34,15 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider<RoomInv
),
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),
anInvitableUser(aMatrixUser("@alice:server.org")),
anInvitableUser(aMatrixUser("@bob:server.org", "Bob")),
anInvitableUser(aMatrixUser("@carol:server.org", "Carol"), isSelected = true),
anInvitableUser(aMatrixUser("@eve:server.org", "Eve"), isSelected = true, isAlreadyJoined = true),
anInvitableUser(aMatrixUser("@justin:server.org", "Justin"), isSelected = true, isAlreadyInvited = true),
)
)
),
aRoomInviteMembersState(
aDefaultInvitePeopleState(
isSearchActive = true,
canInvite = true,
searchQuery = "@alice:server.org",
@ -50,34 +51,51 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider<RoomInv
),
searchResults = SearchBarResultState.Results(
persistentListOf(
InvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
InvitableUser(aMatrixUser("@bob:server.org", "Bob")),
anInvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
anInvitableUser(aMatrixUser("@bob:server.org", "Bob")),
)
)
),
aRoomInviteMembersState(
aDefaultInvitePeopleState(
isSearchActive = true,
canInvite = true,
searchQuery = "@alice:server.org",
searchResults = SearchBarResultState.Results(
persistentListOf(
InvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
anInvitableUser(aMatrixUser("@alice:server.org"), isUnresolved = true),
)
),
showSearchLoader = true,
),
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
)
}
private fun aRoomInviteMembersState(
private fun anInvitableUser(
matrixUser: MatrixUser,
isSelected: Boolean = false,
isAlreadyJoined: Boolean = false,
isAlreadyInvited: Boolean = false,
isUnresolved: Boolean = false,
) = InvitableUser(
matrixUser = matrixUser,
isSelected = isSelected,
isAlreadyJoined = isAlreadyJoined,
isAlreadyInvited = isAlreadyInvited,
isUnresolved = isUnresolved,
)
private fun aDefaultInvitePeopleState(
room: AsyncData<Unit> = AsyncData.Success(Unit),
canInvite: Boolean = false,
searchQuery: String = "",
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.Initial(),
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
isSearchActive: Boolean = false,
showSearchLoader: Boolean = false,
): RoomInviteMembersState {
return RoomInviteMembersState(
): DefaultInvitePeopleState {
return DefaultInvitePeopleState(
room = room,
canInvite = canInvite,
searchQuery = searchQuery,
searchResults = searchResults,

View file

@ -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,
val isAlreadyJoined: Boolean,
val isAlreadyInvited: Boolean,
val isUnresolved: Boolean,
)

View file

@ -0,0 +1,210 @@
/*
* 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.architecture.AsyncData
import io.element.android.libraries.designsystem.components.async.AsyncFailure
import io.element.android.libraries.designsystem.components.async.AsyncLoading
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.designsystem.theme.components.Text
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,
modifier: Modifier = Modifier,
) {
when (state.room) {
is AsyncData.Failure -> InvitePeopleViewError(state.room.error, modifier)
AsyncData.Uninitialized,
is AsyncData.Loading,
is AsyncData.Success -> InvitePeopleContentView(state, modifier)
}
}
@Composable
private fun InvitePeopleViewError(
error: Throwable,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
AsyncFailure(
throwable = error,
onRetry = null,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}
@Composable
private fun InvitePeopleContentView(
state: DefaultInvitePeopleState,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
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 InvitePeopleSearchBar(
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 invitedOrJoined = invitableUser.isAlreadyInvited || invitableUser.isAlreadyJoined
val isUnresolved = invitableUser.isUnresolved && !invitedOrJoined
val enabled = isUnresolved || !invitedOrJoined
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_invite_users_already_a_member)
invitableUser.isAlreadyInvited -> stringResource(R.string.screen_invite_users_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 || invitedOrJoined,
enabled = enabled,
data = data,
onCheckedChange = { onToggleUser(invitableUser.matrixUser) },
modifier = Modifier.fillMaxWidth()
)
if (index < results.lastIndex) {
HorizontalDivider()
}
}
}
},
)
}
@PreviewsDayNight
@Composable
internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =
ElementPreview {
InvitePeopleView(state = state)
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Ужо ўдзельнік"</string>
<string name="screen_invite_users_already_invited">"Ужо запрасілі"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Вече е член"</string>
<string name="screen_invite_users_already_invited">"Вече е бил поканен"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Již členem"</string>
<string name="screen_invite_users_already_invited">"Již pozván(a)"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Eisoes yn aelod"</string>
<string name="screen_invite_users_already_invited">"Wedi gwahodd yn barod"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Allerede medlem"</string>
<string name="screen_invite_users_already_invited">"Allerede inviteret"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Bereits Mitglied"</string>
<string name="screen_invite_users_already_invited">"Bereits eingeladen"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Ήδη μέλος"</string>
<string name="screen_invite_users_already_invited">"Ήδη προσκεκλημένος"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Ya eres miembro"</string>
<string name="screen_invite_users_already_invited">"Ya estás invitado"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Sa juba oled jututoa liige"</string>
<string name="screen_invite_users_already_invited">"Sa juba oled kutse saanud"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Kidea da dagoeneko"</string>
<string name="screen_invite_users_already_invited">"Lehendik ere gonbidatuta"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"از پیش عضو است"</string>
<string name="screen_invite_users_already_invited">"از پیش دعوت شده"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"On jo jäsen"</string>
<string name="screen_invite_users_already_invited">"On jo kutsuttu"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Déjà membre"</string>
<string name="screen_invite_users_already_invited">"Déjà invité(e)"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Már tag"</string>
<string name="screen_invite_users_already_invited">"Már meghívták"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Sudah menjadi anggota"</string>
<string name="screen_invite_users_already_invited">"Sudah diundang"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Già membro"</string>
<string name="screen_invite_users_already_invited">"Già invitato"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"უკვე წევრია"</string>
<string name="screen_invite_users_already_invited">"უკვე მოწვეულია"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Jau narys"</string>
<string name="screen_invite_users_already_invited">"Jau pakviestas"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Allerede medlem"</string>
<string name="screen_invite_users_already_invited">"Allerede invitert"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Reeds lid"</string>
<string name="screen_invite_users_already_invited">"Reeds uitgenodigd"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Jest już członkiem"</string>
<string name="screen_invite_users_already_invited">"Już zaproszony"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Já é membro"</string>
<string name="screen_invite_users_already_invited">"Já foi convidado"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Já é participante"</string>
<string name="screen_invite_users_already_invited">"Já foi convidado"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Deja membru"</string>
<string name="screen_invite_users_already_invited">"Deja invitat"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more