refactor (start chat) : start splitting things (create room, invite people, start chat)
This commit is contained in:
parent
ccb466bad8
commit
41cf1afce3
133 changed files with 2613 additions and 397 deletions
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom
|
||||
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.features.createroom.impl.CreateRoomFlowNode.NavTarget
|
||||
import io.element.android.libraries.architecture.overlay.Overlay
|
||||
import io.element.android.libraries.architecture.overlay.operation.hide
|
||||
import io.element.android.libraries.architecture.overlay.operation.show
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
|
||||
interface CreateRoomNavigator : Plugin {
|
||||
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
|
||||
fun onCreateNewRoom()
|
||||
fun onShowJoinRoomByAddress()
|
||||
fun onDismissJoinRoomByAddress()
|
||||
fun onOpenRoomDirectory()
|
||||
}
|
||||
|
||||
class DefaultCreateRoomNavigator(
|
||||
private val backstack: BackStack<NavTarget>,
|
||||
private val overlay: Overlay<NavTarget>,
|
||||
private val openRoom: (RoomIdOrAlias, List<String>) -> Unit,
|
||||
private val openRoomDirectory: () -> Unit,
|
||||
) : CreateRoomNavigator {
|
||||
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) = openRoom(roomIdOrAlias, serverNames)
|
||||
|
||||
override fun onOpenRoomDirectory() = openRoomDirectory()
|
||||
|
||||
override fun onCreateNewRoom() {
|
||||
backstack.push(NavTarget.NewRoom)
|
||||
}
|
||||
|
||||
override fun onShowJoinRoomByAddress() {
|
||||
overlay.show(NavTarget.JoinByAddress)
|
||||
}
|
||||
|
||||
override fun onDismissJoinRoomByAddress() {
|
||||
overlay.hide()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,19 @@
|
|||
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 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.libraries.architecture.BackstackView
|
||||
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
||||
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 +29,24 @@ 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 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.NewRoom -> {
|
||||
createNode<ConfigureRoomFlowNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.JoinByAddress -> {
|
||||
createNode<JoinRoomByAddressNode>(buildContext = buildContext, plugins = listOf(navigator))
|
||||
}
|
||||
NavTarget.ConfigureRoom -> createNode<ConfigureRoomNode>(buildContext)
|
||||
is NavTarget.AddPeople -> createNode<AddPeopleNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
BackstackView()
|
||||
OverlayView(transitionHandler = remember { JumpToEndTransitionHandler() })
|
||||
}
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object ConfigureRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class AddPeople(val roomId: RoomId) : NavTarget
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -9,27 +9,12 @@ package io.element.android.features.createroom.impl
|
|||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : CreateRoomEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
class DefaultCreateRoomEntryPoint @Inject constructor(): CreateRoomEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<CreateRoomFlowNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +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 androidx.compose.runtime.MutableState
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.StartDMResult
|
||||
import io.element.android.libraries.matrix.api.room.startDM
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultStartDMAction @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : StartDMAction {
|
||||
override suspend fun execute(
|
||||
matrixUser: MatrixUser,
|
||||
createIfDmDoesNotExist: Boolean,
|
||||
actionState: MutableState<AsyncAction<RoomId>>,
|
||||
) {
|
||||
actionState.value = AsyncAction.Loading
|
||||
when (val result = matrixClient.startDM(matrixUser.userId, createIfDmDoesNotExist)) {
|
||||
is StartDMResult.Success -> {
|
||||
if (result.isNew) {
|
||||
analyticsService.capture(CreatedRoom(isDM = true))
|
||||
}
|
||||
actionState.value = AsyncAction.Success(result.roomId)
|
||||
}
|
||||
is StartDMResult.Failure -> {
|
||||
actionState.value = AsyncAction.Failure(result.throwable)
|
||||
}
|
||||
StartDMResult.DmDoesNotExist -> {
|
||||
actionState.value = ConfirmingStartDmWithMatrixUser(matrixUser = matrixUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -7,39 +7,11 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
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
|
||||
|
||||
@ContributesNode(CreateRoomScope::class)
|
||||
class AddPeopleNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AddPeoplePresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onContinue()
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
plugins<Callback>().forEach { it.onContinue() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
AddPeopleView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClick = this::navigateUp,
|
||||
onNextClick = this::onContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
class AddPeopleNode(
|
||||
buildContext: BuildContext,
|
||||
plugins: List<Plugin>,
|
||||
) : Node(buildContext, plugins = plugins)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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.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 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.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.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,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
AddPeopleViewTopBar(
|
||||
hasSelectedUsers = state.selectedUsers.isNotEmpty(),
|
||||
onBackClick = {
|
||||
if (state.isSearchActive) {
|
||||
state.eventSink(UserListEvents.OnSearchActiveChanged(false))
|
||||
} else {
|
||||
onBackClick()
|
||||
}
|
||||
},
|
||||
onNextClick = onNextClick,
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
UserListView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
state = state,
|
||||
showBackButton = false,
|
||||
onSelectUser = {},
|
||||
onDeselectUser = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AddPeopleViewTopBar(
|
||||
hasSelectedUsers: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onNextClick: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(id = R.string.screen_create_room_add_people_title),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
actions = {
|
||||
val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
|
||||
TextButton(
|
||||
text = stringResource(id = textActionResId),
|
||||
onClick = onNextClick,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
|
||||
AddPeopleView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onNextClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,91 +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.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
|
||||
@Composable
|
||||
fun SearchMultipleUsersResultItem(
|
||||
searchResult: UserSearchResult,
|
||||
isUserSelected: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val data = if (searchResult.isUnresolved) {
|
||||
CheckableUserRowData.Unresolved(
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
)
|
||||
} else {
|
||||
CheckableUserRowData.Resolved(
|
||||
name = searchResult.matrixUser.getBestName(),
|
||||
subtext = if (searchResult.matrixUser.displayName.isNullOrEmpty()) null else searchResult.matrixUser.userId.value,
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
)
|
||||
}
|
||||
CheckableUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
data = data,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview {
|
||||
Column {
|
||||
SearchMultipleUsersResultItem(
|
||||
searchResult = UserSearchResult(
|
||||
aMatrixUser(),
|
||||
isUnresolved = false
|
||||
),
|
||||
isUserSelected = false,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
HorizontalDivider()
|
||||
SearchMultipleUsersResultItem(
|
||||
searchResult = UserSearchResult(
|
||||
aMatrixUser(),
|
||||
isUnresolved = false
|
||||
),
|
||||
isUserSelected = true,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
HorizontalDivider()
|
||||
SearchMultipleUsersResultItem(
|
||||
searchResult = UserSearchResult(
|
||||
aMatrixUser(),
|
||||
isUnresolved = true
|
||||
),
|
||||
isUserSelected = false,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
HorizontalDivider()
|
||||
SearchMultipleUsersResultItem(
|
||||
searchResult = UserSearchResult(
|
||||
aMatrixUser(),
|
||||
isUnresolved = true
|
||||
),
|
||||
isUserSelected = true,
|
||||
onCheckedChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +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.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
|
||||
@Composable
|
||||
fun SearchSingleUserResultItem(
|
||||
searchResult: UserSearchResult,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (searchResult.isUnresolved) {
|
||||
UnresolvedUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
)
|
||||
} else {
|
||||
MatrixUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview {
|
||||
Column {
|
||||
SearchSingleUserResultItem(
|
||||
searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false),
|
||||
onClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SearchSingleUserResultItem(
|
||||
searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +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.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
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.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchUserBar(
|
||||
query: String,
|
||||
state: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
showLoader: Boolean,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
isMultiSelectionEnable: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
onTextChange: (String) -> Unit,
|
||||
onUserSelect: (MatrixUser) -> Unit,
|
||||
onUserDeselect: (MatrixUser) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showBackButton: Boolean = true,
|
||||
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
|
||||
) {
|
||||
val columnState = rememberLazyListState()
|
||||
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = onTextChange,
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier,
|
||||
placeHolderTitle = placeHolderTitle,
|
||||
showBackButton = showBackButton,
|
||||
contentPrefix = {
|
||||
if (isMultiSelectionEnable && active && selectedUsers.isNotEmpty()) {
|
||||
// We want the selected users to behave a bit like a top bar - when the list below is scrolled, the colour
|
||||
// should change to indicate elevation.
|
||||
|
||||
val elevation = remember {
|
||||
derivedStateOf {
|
||||
if (columnState.canScrollBackward) {
|
||||
4.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val appBarContainerColor by animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation.value),
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
|
||||
)
|
||||
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
selectedUsers = selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemove = onUserDeselect,
|
||||
modifier = Modifier.background(appBarContainerColor)
|
||||
)
|
||||
}
|
||||
},
|
||||
contentSuffix = {
|
||||
if (showLoader) {
|
||||
AsyncLoading()
|
||||
}
|
||||
},
|
||||
resultState = state,
|
||||
resultHandler = { users ->
|
||||
LazyColumn(state = columnState) {
|
||||
if (isMultiSelectionEnable) {
|
||||
itemsIndexed(users) { index, searchResult ->
|
||||
SearchMultipleUsersResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
searchResult = searchResult,
|
||||
isUserSelected = selectedUsers.contains(searchResult.matrixUser),
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onUserSelect(searchResult.matrixUser)
|
||||
} else {
|
||||
onUserDeselect(searchResult.matrixUser)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (index < users.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(users) { index, searchResult ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
searchResult = searchResult,
|
||||
onClick = { onUserSelect(searchResult.matrixUser) }
|
||||
)
|
||||
if (index < users.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,126 +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.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.userlist.UserListEvents
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
||||
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.ListSectionHeader
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun UserListView(
|
||||
state: UserListState,
|
||||
onSelectUser: (MatrixUser) -> Unit,
|
||||
onDeselectUser: (MatrixUser) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showBackButton: Boolean = true,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
SearchUserBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
query = state.searchQuery,
|
||||
state = state.searchResults,
|
||||
selectedUsers = state.selectedUsers,
|
||||
active = state.isSearchActive,
|
||||
showLoader = state.showSearchLoader,
|
||||
isMultiSelectionEnable = state.isMultiSelectionEnabled,
|
||||
showBackButton = showBackButton,
|
||||
onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
|
||||
onTextChange = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
|
||||
onUserSelect = {
|
||||
state.eventSink(UserListEvents.AddToSelection(it))
|
||||
onSelectUser(it)
|
||||
},
|
||||
onUserDeselect = {
|
||||
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||
onDeselectUser(it)
|
||||
},
|
||||
)
|
||||
|
||||
if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) {
|
||||
SelectedUsersRowList(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
selectedUsers = state.selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemove = {
|
||||
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||
onDeselectUser(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!state.isSearchActive && state.recentDirectRooms.isNotEmpty()) {
|
||||
LazyColumn {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(id = CommonStrings.common_suggestions),
|
||||
hasDivider = false,
|
||||
)
|
||||
}
|
||||
state.recentDirectRooms.forEachIndexed { index, recentDirectRoom ->
|
||||
item {
|
||||
val isSelected = state.selectedUsers.any {
|
||||
recentDirectRoom.matrixUser.userId == it.userId
|
||||
}
|
||||
CheckableUserRow(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
if (isSelected) {
|
||||
state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser))
|
||||
onDeselectUser(recentDirectRoom.matrixUser)
|
||||
} else {
|
||||
state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser))
|
||||
onSelectUser(recentDirectRoom.matrixUser)
|
||||
}
|
||||
},
|
||||
data = CheckableUserRowData.Resolved(
|
||||
avatarData = recentDirectRoom.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
name = recentDirectRoom.matrixUser.getBestName(),
|
||||
subtext = recentDirectRoom.matrixUser.userId.value,
|
||||
),
|
||||
)
|
||||
if (index < state.recentDirectRooms.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview {
|
||||
UserListView(
|
||||
state = state,
|
||||
onSelectUser = {},
|
||||
onDeselectUser = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -16,7 +16,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
|
||||
|
|
|
|||
|
|
@ -13,24 +13,20 @@ import com.bumble.appyx.core.lifecycle.subscribe
|
|||
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 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.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()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
|
@ -48,7 +44,7 @@ class ConfigureRoomNode @AssistedInject constructor(
|
|||
modifier = modifier,
|
||||
onBackClick = this::navigateUp,
|
||||
onCreateRoomSuccess = {
|
||||
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
|
||||
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -5,45 +5,30 @@
|
|||
* 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.asStateFlow
|
||||
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 ->
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.joinbyaddress
|
||||
|
||||
sealed interface JoinRoomByAddressEvents {
|
||||
data object Dismiss : JoinRoomByAddressEvents
|
||||
data object Continue : JoinRoomByAddressEvents
|
||||
data class UpdateAddress(val address: String) : JoinRoomByAddressEvents
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.joinbyaddress
|
||||
|
||||
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 dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.createroom.CreateRoomNavigator
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class JoinRoomByAddressNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: JoinRoomByAddressPresenter.Factory,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||
private val presenter = presenterFactory.create(navigator)
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
JoinRoomByAddressView(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.joinbyaddress
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.createroom.CreateRoomNavigator
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS = 10
|
||||
|
||||
class JoinRoomByAddressPresenter @AssistedInject constructor(
|
||||
@Assisted private val navigator: CreateRoomNavigator,
|
||||
private val client: MatrixClient,
|
||||
private val roomAliasHelper: RoomAliasHelper,
|
||||
) : Presenter<JoinRoomByAddressState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(navigator: CreateRoomNavigator): JoinRoomByAddressPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): JoinRoomByAddressState {
|
||||
var address by remember { mutableStateOf("") }
|
||||
var internalAddressState by remember { mutableStateOf<RoomAddressState>(RoomAddressState.Unknown) }
|
||||
var validateAddress: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleEvents(event: JoinRoomByAddressEvents) {
|
||||
when (event) {
|
||||
JoinRoomByAddressEvents.Continue -> {
|
||||
when (val currentState = internalAddressState) {
|
||||
is RoomAddressState.RoomFound -> onRoomFound(currentState)
|
||||
else -> validateAddress = true
|
||||
}
|
||||
}
|
||||
JoinRoomByAddressEvents.Dismiss -> navigator.onDismissJoinRoomByAddress()
|
||||
is JoinRoomByAddressEvents.UpdateAddress -> {
|
||||
validateAddress = false
|
||||
address = event.address.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoomAddressStateEffect(
|
||||
fullAddress = address,
|
||||
onRoomAddressStateChange = { addressState ->
|
||||
internalAddressState = addressState
|
||||
if (addressState is RoomAddressState.RoomFound && validateAddress) {
|
||||
onRoomFound(addressState)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val addressState by remember {
|
||||
derivedStateOf {
|
||||
// We only want to show the "RoomFound" state as long as the user didn't validate the address.
|
||||
if (validateAddress || internalAddressState is RoomAddressState.RoomFound) {
|
||||
internalAddressState
|
||||
} else {
|
||||
RoomAddressState.Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return JoinRoomByAddressState(
|
||||
address = address,
|
||||
addressState = addressState,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRoomFound(state: RoomAddressState.RoomFound) {
|
||||
navigator.onDismissJoinRoomByAddress()
|
||||
navigator.onOpenRoom(
|
||||
roomIdOrAlias = state.resolved.roomId.toRoomIdOrAlias(),
|
||||
serverNames = state.resolved.servers
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomAddressStateEffect(
|
||||
fullAddress: String,
|
||||
onRoomAddressStateChange: (RoomAddressState) -> Unit,
|
||||
) {
|
||||
val onChange by rememberUpdatedState(onRoomAddressStateChange)
|
||||
LaunchedEffect(fullAddress) {
|
||||
// Whenever the address changes, reset the state to unknown
|
||||
onChange(RoomAddressState.Unknown)
|
||||
// debounce the room address resolution
|
||||
delay(300)
|
||||
val roomAlias = tryOrNull { RoomAlias(fullAddress) }
|
||||
if (roomAlias != null && roomAliasHelper.isRoomAliasValid(roomAlias)) {
|
||||
onChange(RoomAddressState.Resolving)
|
||||
onChange(client.resolveRoomAddress(roomAlias))
|
||||
} else {
|
||||
onChange(RoomAddressState.Invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MatrixClient.resolveRoomAddress(roomAlias: RoomAlias): RoomAddressState {
|
||||
return withTimeoutOrNull(ADDRESS_RESOLVE_TIMEOUT_IN_SECONDS.seconds) {
|
||||
resolveRoomAlias(roomAlias)
|
||||
.fold(
|
||||
onSuccess = { resolved ->
|
||||
if (resolved.isPresent) {
|
||||
RoomAddressState.RoomFound(resolved.get())
|
||||
} else {
|
||||
RoomAddressState.RoomNotFound
|
||||
}
|
||||
},
|
||||
onFailure = { _ -> RoomAddressState.RoomNotFound }
|
||||
)
|
||||
} ?: RoomAddressState.RoomNotFound
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.joinbyaddress
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
|
||||
data class JoinRoomByAddressState(
|
||||
val address: String,
|
||||
val addressState: RoomAddressState,
|
||||
val eventSink: (JoinRoomByAddressEvents) -> Unit
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed interface RoomAddressState {
|
||||
data object Unknown : RoomAddressState
|
||||
data object Invalid : RoomAddressState
|
||||
data object Resolving : RoomAddressState
|
||||
data object RoomNotFound : RoomAddressState
|
||||
data class RoomFound(val resolved: ResolvedRoomAlias) : RoomAddressState
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.joinbyaddress
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
|
||||
open class JoinRoomByAddressStateProvider : PreviewParameterProvider<JoinRoomByAddressState> {
|
||||
override val values: Sequence<JoinRoomByAddressState>
|
||||
get() = sequenceOf(
|
||||
aJoinRoomByAddressState(),
|
||||
aJoinRoomByAddressState(address = "#room-"),
|
||||
aJoinRoomByAddressState(address = "#room-", addressState = RoomAddressState.Invalid),
|
||||
aJoinRoomByAddressState(address = "#room-name:matrix.org", addressState = RoomAddressState.Resolving),
|
||||
aJoinRoomByAddressState(address = "#room-name-none:matrix.org", addressState = RoomAddressState.RoomNotFound),
|
||||
aJoinRoomByAddressState(
|
||||
address = "#room-name:matrix.org",
|
||||
addressState = RoomAddressState.RoomFound(ResolvedRoomAlias(RoomId("!aRoom:id"), emptyList())),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aJoinRoomByAddressState(
|
||||
address: String = "",
|
||||
addressState: RoomAddressState = RoomAddressState.Unknown,
|
||||
eventSink: (JoinRoomByAddressEvents) -> Unit = {},
|
||||
) = JoinRoomByAddressState(
|
||||
address = address,
|
||||
addressState = addressState,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.createroom.impl.joinbyaddress
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun JoinRoomByAddressView(
|
||||
state: JoinRoomByAddressState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
state.eventSink(JoinRoomByAddressEvents.Dismiss)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RoomAddressField(
|
||||
address = state.address,
|
||||
addressState = state.addressState,
|
||||
requestFocus = sheetState.isVisible,
|
||||
onAddressChange = {
|
||||
state.eventSink(JoinRoomByAddressEvents.UpdateAddress(it))
|
||||
},
|
||||
onContinue = {
|
||||
state.eventSink(JoinRoomByAddressEvents.Continue)
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_continue),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = state.addressState is RoomAddressState.Resolving,
|
||||
onClick = {
|
||||
state.eventSink(JoinRoomByAddressEvents.Continue)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomAddressField(
|
||||
address: String,
|
||||
addressState: RoomAddressState,
|
||||
requestFocus: Boolean,
|
||||
onAddressChange: (String) -> Unit,
|
||||
onContinue: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
if (requestFocus) {
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
TextField(
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
value = address,
|
||||
label = stringResource(R.string.screen_start_chat_join_room_by_address_action),
|
||||
placeholder = stringResource(R.string.screen_start_chat_join_room_by_address_placeholder),
|
||||
supportingText = when (addressState) {
|
||||
RoomAddressState.Invalid -> stringResource(R.string.screen_start_chat_join_room_by_address_invalid_address)
|
||||
is RoomAddressState.RoomFound -> stringResource(R.string.screen_start_chat_join_room_by_address_room_found)
|
||||
RoomAddressState.RoomNotFound -> stringResource(R.string.screen_start_chat_join_room_by_address_room_not_found)
|
||||
RoomAddressState.Unknown, RoomAddressState.Resolving -> stringResource(R.string.screen_start_chat_join_room_by_address_supporting_text)
|
||||
},
|
||||
validity = when (addressState) {
|
||||
RoomAddressState.Unknown, RoomAddressState.Resolving -> TextFieldValidity.None
|
||||
RoomAddressState.Invalid, RoomAddressState.RoomNotFound -> TextFieldValidity.Invalid
|
||||
is RoomAddressState.RoomFound -> TextFieldValidity.Valid
|
||||
},
|
||||
onValueChange = onAddressChange,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Uri,
|
||||
imeAction = ImeAction.Go
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = { onContinue() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun JoinRoomByAddressViewPreview(
|
||||
@PreviewParameter(JoinRoomByAddressStateProvider::class) state: JoinRoomByAddressState
|
||||
) = ElementPreview {
|
||||
JoinRoomByAddressView(state = state)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,66 +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 android.app.Activity
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
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 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.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateRoomRootNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: CreateRoomRootPresenter,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val navigator = plugins<CreateRoomNavigator>().first()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = { analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.StartChat)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
CreateRoomRootView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onCloseClick = this::navigateUp,
|
||||
onNewRoomClick = navigator::onCreateNewRoom,
|
||||
onOpenDM = {
|
||||
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
|
||||
},
|
||||
onJoinByAddressClick = navigator::onShowJoinRoomByAddress,
|
||||
onInviteFriendsClick = { invitePeople(activity) },
|
||||
onRoomDirectorySearchClick = navigator::onOpenRoomDirectory
|
||||
)
|
||||
}
|
||||
|
||||
private fun invitePeople(activity: Activity) {
|
||||
inviteFriendsUseCase.execute(activity)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +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 androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.features.createroom.impl.userlist.SelectionMode
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateRoomRootPresenter @Inject constructor(
|
||||
presenterFactory: UserListPresenter.Factory,
|
||||
userRepository: UserRepository,
|
||||
userListDataStore: UserListDataStore,
|
||||
private val startDMAction: StartDMAction,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<CreateRoomRootState> {
|
||||
private val presenter = presenterFactory.create(
|
||||
UserListPresenterArgs(
|
||||
selectionMode = SelectionMode.Single,
|
||||
),
|
||||
userRepository,
|
||||
userListDataStore,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): CreateRoomRootState {
|
||||
val userListState = presenter.present()
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
val isRoomDirectorySearchEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch)
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
|
||||
startDMAction.execute(
|
||||
matrixUser = event.matrixUser,
|
||||
createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
|
||||
actionState = startDmActionState,
|
||||
)
|
||||
}
|
||||
CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
return CreateRoomRootState(
|
||||
applicationName = buildMeta.applicationName,
|
||||
userListState = userListState,
|
||||
startDmAction = startDmActionState.value,
|
||||
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +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.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class CreateRoomRootState(
|
||||
val applicationName: String,
|
||||
val userListState: UserListState,
|
||||
val startDmAction: AsyncAction<RoomId>,
|
||||
val isRoomDirectorySearchEnabled: Boolean,
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -1,74 +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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
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.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRootState> {
|
||||
override val values: Sequence<CreateRoomRootState>
|
||||
get() = sequenceOf(
|
||||
aCreateRoomRootState(),
|
||||
aCreateRoomRootState(
|
||||
startDmAction = AsyncAction.Loading,
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.userId.value,
|
||||
searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
aCreateRoomRootState(
|
||||
startDmAction = AsyncAction.Failure(RuntimeException("error")),
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.userId.value,
|
||||
searchResults = SearchBarResultState.Results(persistentListOf(UserSearchResult(it, false))),
|
||||
selectedUsers = persistentListOf(it),
|
||||
isSearchActive = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
aCreateRoomRootState(
|
||||
userListState = aUserListState(
|
||||
recentDirectRooms = aRecentDirectRoomList()
|
||||
)
|
||||
),
|
||||
aCreateRoomRootState(
|
||||
startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()),
|
||||
),
|
||||
aCreateRoomRootState(
|
||||
isRoomDirectorySearchEnabled = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aCreateRoomRootState(
|
||||
applicationName: String = "Element X Preview",
|
||||
userListState: UserListState = aUserListState(),
|
||||
startDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
isRoomDirectorySearchEnabled: Boolean = false,
|
||||
eventSink: (CreateRoomRootEvents) -> Unit = {},
|
||||
) = CreateRoomRootState(
|
||||
applicationName = applicationName,
|
||||
userListState = userListState,
|
||||
startDmAction = startDmAction,
|
||||
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -1,253 +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 androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.createroom.api.ConfirmingStartDmWithMatrixUser
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.createroom.impl.components.UserListView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
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.CreateDmConfirmationBottomSheet
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun CreateRoomRootView(
|
||||
state: CreateRoomRootState,
|
||||
onCloseClick: () -> Unit,
|
||||
onNewRoomClick: () -> Unit,
|
||||
onOpenDM: (RoomId) -> Unit,
|
||||
onInviteFriendsClick: () -> Unit,
|
||||
onJoinByAddressClick: () -> Unit,
|
||||
onRoomDirectorySearchClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
topBar = {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
CreateRoomRootViewTopBar(onCloseClick = onCloseClick)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
UserListView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
// Do not render suggestions in this case, the suggestion will be rendered
|
||||
// by CreateRoomActionButtonsList
|
||||
state = state.userListState.copy(
|
||||
recentDirectRooms = persistentListOf(),
|
||||
),
|
||||
onSelectUser = {
|
||||
state.eventSink(CreateRoomRootEvents.StartDM(it))
|
||||
},
|
||||
onDeselectUser = { },
|
||||
)
|
||||
|
||||
if (!state.userListState.isSearchActive) {
|
||||
CreateRoomActionButtonsList(
|
||||
state = state,
|
||||
onNewRoomClick = onNewRoomClick,
|
||||
onInvitePeopleClick = onInviteFriendsClick,
|
||||
onJoinByAddressClick = onJoinByAddressClick,
|
||||
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
|
||||
onDmClick = onOpenDM,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncActionView(
|
||||
async = state.startDmAction,
|
||||
progressDialog = {
|
||||
AsyncActionViewDefaults.ProgressDialog(
|
||||
progressText = stringResource(CommonStrings.common_starting_chat),
|
||||
)
|
||||
},
|
||||
onSuccess = { onOpenDM(it) },
|
||||
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
|
||||
onRetry = {
|
||||
state.userListState.selectedUsers.firstOrNull()
|
||||
?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) }
|
||||
// Cancel start DM if there is no more selected user (should not happen)
|
||||
?: state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
},
|
||||
onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
||||
confirmationDialog = { data ->
|
||||
if (data is ConfirmingStartDmWithMatrixUser) {
|
||||
CreateDmConfirmationBottomSheet(
|
||||
matrixUser = data.matrixUser,
|
||||
onSendInvite = {
|
||||
state.eventSink(CreateRoomRootEvents.StartDM(data.matrixUser))
|
||||
},
|
||||
onDismiss = {
|
||||
state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CreateRoomRootViewTopBar(
|
||||
onCloseClick: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(id = CommonStrings.action_start_chat),
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
onClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateRoomActionButtonsList(
|
||||
state: CreateRoomRootState,
|
||||
onNewRoomClick: () -> Unit,
|
||||
onInvitePeopleClick: () -> Unit,
|
||||
onJoinByAddressClick: () -> Unit,
|
||||
onRoomDirectorySearchClick: () -> Unit,
|
||||
onDmClick: (RoomId) -> Unit,
|
||||
) {
|
||||
LazyColumn {
|
||||
item {
|
||||
CreateRoomActionButton(
|
||||
iconRes = CompoundDrawables.ic_compound_plus,
|
||||
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
||||
onClick = onNewRoomClick,
|
||||
)
|
||||
}
|
||||
if (state.isRoomDirectorySearchEnabled) {
|
||||
item {
|
||||
CreateRoomActionButton(
|
||||
iconRes = CompoundDrawables.ic_compound_list_bulleted,
|
||||
text = stringResource(id = R.string.screen_room_directory_search_title),
|
||||
onClick = onRoomDirectorySearchClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
CreateRoomActionButton(
|
||||
iconRes = CompoundDrawables.ic_compound_share_android,
|
||||
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
|
||||
onClick = onInvitePeopleClick,
|
||||
)
|
||||
}
|
||||
item {
|
||||
CreateRoomActionButton(
|
||||
iconRes = CompoundDrawables.ic_compound_room,
|
||||
text = stringResource(R.string.screen_start_chat_join_room_by_address_action),
|
||||
onClick = onJoinByAddressClick,
|
||||
)
|
||||
}
|
||||
if (state.userListState.recentDirectRooms.isNotEmpty()) {
|
||||
item {
|
||||
ListSectionHeader(
|
||||
title = stringResource(id = CommonStrings.common_suggestions),
|
||||
hasDivider = false,
|
||||
)
|
||||
}
|
||||
state.userListState.recentDirectRooms.forEach { recentDirectRoom ->
|
||||
item {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable(
|
||||
onClick = {
|
||||
onDmClick(recentDirectRoom.roomId)
|
||||
}
|
||||
),
|
||||
matrixUser = recentDirectRoom.matrixUser,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateRoomActionButton(
|
||||
@DrawableRes iconRes: Int,
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
resourceId = iconRes,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) =
|
||||
ElementPreview {
|
||||
CreateRoomRootView(
|
||||
state = state,
|
||||
onCloseClick = {},
|
||||
onNewRoomClick = {},
|
||||
onOpenDM = {},
|
||||
onJoinByAddressClick = {},
|
||||
onInviteFriendsClick = {},
|
||||
onRoomDirectorySearchClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,95 +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.userlist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
|
||||
import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class DefaultUserListPresenter @AssistedInject constructor(
|
||||
@Assisted val args: UserListPresenterArgs,
|
||||
@Assisted val userRepository: UserRepository,
|
||||
@Assisted val userListDataStore: UserListDataStore,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : UserListPresenter {
|
||||
@AssistedFactory
|
||||
@ContributesBinding(SessionScope::class)
|
||||
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||
override fun create(
|
||||
args: UserListPresenterArgs,
|
||||
userRepository: UserRepository,
|
||||
userListDataStore: UserListDataStore,
|
||||
): DefaultUserListPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): UserListState {
|
||||
var recentDirectRooms by remember { mutableStateOf(emptyList<RecentDirectRoom>()) }
|
||||
LaunchedEffect(Unit) {
|
||||
recentDirectRooms = matrixClient.getRecentDirectRooms()
|
||||
}
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val selectedUsers by userListDataStore.selectedUsers.collectAsState(emptyList())
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
|
||||
mutableStateOf(SearchBarResultState.Initial())
|
||||
}
|
||||
var showSearchLoader by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
searchResults = SearchBarResultState.Initial()
|
||||
showSearchLoader = false
|
||||
userRepository.search(searchQuery).onEach { state ->
|
||||
showSearchLoader = state.isSearching
|
||||
searchResults = when {
|
||||
state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial()
|
||||
state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound()
|
||||
else -> SearchBarResultState.Results(state.results.toImmutableList())
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
return UserListState(
|
||||
searchQuery = searchQuery,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
isSearchActive = isSearchActive,
|
||||
showSearchLoader = showSearchLoader,
|
||||
selectionMode = args.selectionMode,
|
||||
recentDirectRooms = recentDirectRooms.toImmutableList(),
|
||||
eventSink = { event ->
|
||||
when (event) {
|
||||
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||
is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser)
|
||||
is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +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.userlist
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserListDataStore @Inject constructor() {
|
||||
private val _selectedUsers: MutableStateFlow<List<MatrixUser>> = MutableStateFlow(emptyList())
|
||||
|
||||
fun selectUser(user: MatrixUser) {
|
||||
if (!_selectedUsers.value.contains(user)) {
|
||||
_selectedUsers.tryEmit(_selectedUsers.value.plus(user))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeUserFromSelection(user: MatrixUser) {
|
||||
_selectedUsers.tryEmit(_selectedUsers.value.minus(user))
|
||||
}
|
||||
|
||||
val selectedUsers = _selectedUsers.asStateFlow()
|
||||
}
|
||||
|
|
@ -1,17 +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.userlist
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface UserListEvents {
|
||||
data class UpdateSearchQuery(val query: String) : UserListEvents
|
||||
data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents
|
||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents
|
||||
data class OnSearchActiveChanged(val active: Boolean) : UserListEvents
|
||||
}
|
||||
|
|
@ -1,21 +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.userlist
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.usersearch.api.UserRepository
|
||||
|
||||
interface UserListPresenter : Presenter<UserListState> {
|
||||
interface Factory {
|
||||
fun create(
|
||||
args: UserListPresenterArgs,
|
||||
userRepository: UserRepository,
|
||||
userListDataStore: UserListDataStore,
|
||||
): UserListPresenter
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +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.userlist
|
||||
|
||||
data class UserListPresenterArgs(
|
||||
val selectionMode: SelectionMode,
|
||||
)
|
||||
|
||||
enum class SelectionMode {
|
||||
Single,
|
||||
Multiple,
|
||||
}
|
||||
|
|
@ -1,27 +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.userlist
|
||||
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class UserListState(
|
||||
val searchQuery: String,
|
||||
val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
val showSearchLoader: Boolean,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val selectionMode: SelectionMode,
|
||||
val recentDirectRooms: ImmutableList<RecentDirectRoom>,
|
||||
val eventSink: (UserListEvents) -> Unit,
|
||||
) {
|
||||
val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple
|
||||
}
|
||||
|
|
@ -1,90 +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.userlist
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||
override val values: Sequence<UserListState>
|
||||
get() = sequenceOf(
|
||||
aUserListState(),
|
||||
aUserListState(
|
||||
isSearchActive = false,
|
||||
selectedUsers = aListOfSelectedUsers(),
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
),
|
||||
aUserListState(isSearchActive = true),
|
||||
aUserListState(isSearchActive = true, searchQuery = "someone"),
|
||||
aUserListState(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple),
|
||||
aUserListState(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
||||
),
|
||||
aUserListState(
|
||||
isSearchActive = true,
|
||||
searchQuery = "@someone:matrix.org",
|
||||
selectionMode = SelectionMode.Multiple,
|
||||
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||
searchResults = SearchBarResultState.Results(aListOfUserSearchResults()),
|
||||
),
|
||||
aUserListState(
|
||||
isSearchActive = true,
|
||||
searchQuery = "something-with-no-results",
|
||||
searchResults = SearchBarResultState.NoResultsFound()
|
||||
),
|
||||
aUserListState(
|
||||
isSearchActive = true,
|
||||
searchQuery = "someone",
|
||||
selectionMode = SelectionMode.Single,
|
||||
),
|
||||
aUserListState(
|
||||
recentDirectRooms = aRecentDirectRoomList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aUserListState(
|
||||
searchQuery: String = "",
|
||||
isSearchActive: Boolean = false,
|
||||
searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> = SearchBarResultState.Initial(),
|
||||
selectedUsers: List<MatrixUser> = emptyList(),
|
||||
showSearchLoader: Boolean = false,
|
||||
selectionMode: SelectionMode = SelectionMode.Single,
|
||||
recentDirectRooms: List<RecentDirectRoom> = emptyList(),
|
||||
eventSink: (UserListEvents) -> Unit = {},
|
||||
) = UserListState(
|
||||
searchQuery = searchQuery,
|
||||
isSearchActive = isSearchActive,
|
||||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
showSearchLoader = showSearchLoader,
|
||||
selectionMode = selectionMode,
|
||||
recentDirectRooms = recentDirectRooms.toImmutableList(),
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList()
|
||||
fun aListOfUserSearchResults() = aMatrixUserList().take(6).map { UserSearchResult(it) }.toImmutableList()
|
||||
|
||||
fun aRecentDirectRoomList(
|
||||
count: Int = 5
|
||||
): List<RecentDirectRoom> = aMatrixUserList()
|
||||
.take(count)
|
||||
.map {
|
||||
RecentDirectRoom(RoomId("!aRoom:id"), it)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue