refactor (start chat) : remove useless code
This commit is contained in:
parent
50073389c4
commit
98343e0531
24 changed files with 17 additions and 1556 deletions
|
|
@ -11,9 +11,15 @@ import android.net.Uri
|
||||||
import app.cash.turbine.TurbineTestContext
|
import app.cash.turbine.TurbineTestContext
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.invitepeople.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
|
||||||
import io.element.android.features.invitepeople.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
|
||||||
import io.element.android.features.invitepeople.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfigStore
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomAccess
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomAddress
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
|
@ -28,7 +34,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
|
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
|
|
@ -48,8 +53,6 @@ import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -67,7 +70,7 @@ private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
|
||||||
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
|
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class ConfigureBaseRoomPresenterTest {
|
class ConfigureRoomPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
|
@ -124,12 +127,11 @@ class ConfigureBaseRoomPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - state is updated when fields are changed`() = runTest {
|
fun `present - state is updated when fields are changed`() = runTest {
|
||||||
val userListDataStore = UserListDataStore()
|
|
||||||
val pickerProvider = FakePickerProvider()
|
val pickerProvider = FakePickerProvider()
|
||||||
val permissionsPresenter = FakePermissionsPresenter()
|
val permissionsPresenter = FakePermissionsPresenter()
|
||||||
val roomAliasHelper = FakeRoomAliasHelper()
|
val roomAliasHelper = FakeRoomAliasHelper()
|
||||||
val presenter = createConfigureRoomPresenter(
|
val presenter = createConfigureRoomPresenter(
|
||||||
createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
|
dataStore = CreateRoomConfigStore(roomAliasHelper),
|
||||||
pickerProvider = pickerProvider,
|
pickerProvider = pickerProvider,
|
||||||
permissionsPresenter = permissionsPresenter,
|
permissionsPresenter = permissionsPresenter,
|
||||||
)
|
)
|
||||||
|
|
@ -138,19 +140,10 @@ class ConfigureBaseRoomPresenterTest {
|
||||||
var expectedConfig = CreateRoomConfig()
|
var expectedConfig = CreateRoomConfig()
|
||||||
assertThat(initialState.config).isEqualTo(expectedConfig)
|
assertThat(initialState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Select User
|
|
||||||
val selectedUser1 = aMatrixUser()
|
|
||||||
val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob")
|
|
||||||
userListDataStore.selectUser(selectedUser1)
|
|
||||||
skipItems(1)
|
|
||||||
userListDataStore.selectUser(selectedUser2)
|
|
||||||
var newState = awaitItem()
|
|
||||||
expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2))
|
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
|
||||||
|
|
||||||
// Room name
|
// Room name
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
||||||
newState = awaitItem()
|
var newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
|
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
|
@ -206,12 +199,6 @@ class ConfigureBaseRoomPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Remove user
|
|
||||||
newState.eventSink(ConfigureRoomEvents.RemoveUserFromSelection(selectedUser1))
|
|
||||||
newState = awaitItem()
|
|
||||||
expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
|
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,16 +250,16 @@ class ConfigureBaseRoomPresenterTest {
|
||||||
val matrixClient = createMatrixClient()
|
val matrixClient = createMatrixClient()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val mediaPreProcessor = FakeMediaPreProcessor()
|
val mediaPreProcessor = FakeMediaPreProcessor()
|
||||||
val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
|
val dataStore = CreateRoomConfigStore( FakeRoomAliasHelper())
|
||||||
val presenter = createConfigureRoomPresenter(
|
val presenter = createConfigureRoomPresenter(
|
||||||
createRoomDataStore = createRoomDataStore,
|
dataStore = dataStore,
|
||||||
mediaPreProcessor = mediaPreProcessor,
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
analyticsService = analyticsService
|
analyticsService = analyticsService
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
createRoomDataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
|
dataStore.setAvatarUri(Uri.parse(AN_URI_FROM_GALLERY))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
|
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(mockk(), mockk(), mockk())))
|
||||||
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
|
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
|
||||||
|
|
@ -405,7 +392,7 @@ class ConfigureBaseRoomPresenterTest {
|
||||||
|
|
||||||
private fun createConfigureRoomPresenter(
|
private fun createConfigureRoomPresenter(
|
||||||
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
|
||||||
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
|
dataStore: CreateRoomConfigStore = CreateRoomConfigStore(roomAliasHelper),
|
||||||
matrixClient: MatrixClient = createMatrixClient(),
|
matrixClient: MatrixClient = createMatrixClient(),
|
||||||
pickerProvider: PickerProvider = FakePickerProvider(),
|
pickerProvider: PickerProvider = FakePickerProvider(),
|
||||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
|
|
@ -414,7 +401,7 @@ class ConfigureBaseRoomPresenterTest {
|
||||||
isKnockFeatureEnabled: Boolean = true,
|
isKnockFeatureEnabled: Boolean = true,
|
||||||
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
|
||||||
) = ConfigureRoomPresenter(
|
) = ConfigureRoomPresenter(
|
||||||
dataStore = createRoomDataStore,
|
dataStore = dataStore,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
mediaPickerProvider = pickerProvider,
|
mediaPickerProvider = pickerProvider,
|
||||||
mediaPreProcessor = mediaPreProcessor,
|
mediaPreProcessor = mediaPreProcessor,
|
||||||
|
|
@ -1,22 +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.startchat.impl
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.RoomVisibilityState
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
|
|
||||||
data class CreateRoomConfig(
|
|
||||||
val roomName: String? = null,
|
|
||||||
val topic: String? = null,
|
|
||||||
val avatarUri: Uri? = null,
|
|
||||||
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
|
||||||
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
|
|
||||||
)
|
|
||||||
|
|
@ -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.startchat.impl
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.RoomAccess
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.RoomAccessItem
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.RoomAddress
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.RoomVisibilityItem
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.RoomVisibilityState
|
|
||||||
import io.element.android.features.startchat.impl.di.CreateRoomScope
|
|
||||||
import io.element.android.features.startchat.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.getAndUpdate
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SingleIn(CreateRoomScope::class)
|
|
||||||
class CreateRoomDataStore @Inject constructor(
|
|
||||||
val selectedUserListDataStore: UserListDataStore,
|
|
||||||
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 setRoomName(roomName: String) {
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
val newVisibility = when (config.roomVisibility) {
|
|
||||||
is RoomVisibilityState.Public -> {
|
|
||||||
val roomAddress = config.roomVisibility.roomAddress
|
|
||||||
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
|
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
|
|
||||||
config.roomVisibility.copy(
|
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
config.roomVisibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> config.roomVisibility
|
|
||||||
}
|
|
||||||
config.copy(
|
|
||||||
roomName = roomName.takeIf { it.isNotEmpty() },
|
|
||||||
roomVisibility = newVisibility,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTopic(topic: String) {
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
config.copy(topic = topic.takeIf { it.isNotEmpty() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAvatarUri(uri: Uri?, cached: Boolean = false) {
|
|
||||||
cachedAvatarUri = uri.takeIf { cached }
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
config.copy(avatarUri = uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRoomVisibility(visibility: RoomVisibilityItem) {
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
config.copy(
|
|
||||||
roomVisibility = when (visibility) {
|
|
||||||
RoomVisibilityItem.Private -> RoomVisibilityState.Private
|
|
||||||
RoomVisibilityItem.Public -> {
|
|
||||||
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
|
|
||||||
RoomVisibilityState.Public(
|
|
||||||
roomAddress = RoomAddress.AutoFilled(roomAliasName),
|
|
||||||
roomAccess = RoomAccess.Anyone,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRoomAddress(address: String) {
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
config.copy(
|
|
||||||
roomVisibility = when (config.roomVisibility) {
|
|
||||||
is RoomVisibilityState.Public -> {
|
|
||||||
val sanitizedAddress = address.lowercase()
|
|
||||||
config.roomVisibility.copy(roomAddress = RoomAddress.Edited(sanitizedAddress))
|
|
||||||
}
|
|
||||||
else -> config.roomVisibility
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRoomAccess(access: RoomAccessItem) {
|
|
||||||
createRoomConfigFlow.getAndUpdate { config ->
|
|
||||||
config.copy(
|
|
||||||
roomVisibility = when (config.roomVisibility) {
|
|
||||||
is RoomVisibilityState.Public -> {
|
|
||||||
when (access) {
|
|
||||||
RoomAccessItem.Anyone -> config.roomVisibility.copy(roomAccess = RoomAccess.Anyone)
|
|
||||||
RoomAccessItem.AskToJoin -> config.roomVisibility.copy(roomAccess = RoomAccess.Knocking)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> config.roomVisibility
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearCachedData() {
|
|
||||||
cachedAvatarUri = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +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.startchat.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.startchat.StartChatNavigator
|
|
||||||
import io.element.android.features.startchat.impl.addpeople.AddPeopleNode
|
|
||||||
import io.element.android.features.startchat.impl.configureroom.ConfigureRoomNode
|
|
||||||
import io.element.android.features.startchat.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 CreateRoomFlowNode @AssistedInject constructor(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
) : DaggerComponentOwner,
|
|
||||||
BaseFlowNode<CreateRoomFlowNode.NavTarget>(
|
|
||||||
backstack = BackStack(
|
|
||||||
initialElement = NavTarget.ConfigureRoom,
|
|
||||||
savedStateMap = buildContext.savedStateMap,
|
|
||||||
),
|
|
||||||
buildContext = buildContext,
|
|
||||||
plugins = plugins
|
|
||||||
) {
|
|
||||||
private val component by lazy {
|
|
||||||
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
|
|
||||||
}
|
|
||||||
private val navigator = plugins<StartChatNavigator>().first()
|
|
||||||
|
|
||||||
override val daggerComponent: Any
|
|
||||||
get() = component
|
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
|
||||||
@Parcelize
|
|
||||||
data object AddPeople : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object ConfigureRoom : NavTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
|
||||||
return when (navTarget) {
|
|
||||||
NavTarget.AddPeople -> {
|
|
||||||
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,45 +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.startchat.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.startchat.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,
|
|
||||||
onSkipClick = this::onContinue,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.startchat.impl.addpeople
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import io.element.android.features.startchat.impl.CreateRoomDataStore
|
|
||||||
import io.element.android.features.startchat.impl.userlist.SelectionMode
|
|
||||||
import io.element.android.features.startchat.impl.userlist.UserListPresenter
|
|
||||||
import io.element.android.features.startchat.impl.userlist.UserListPresenterArgs
|
|
||||||
import io.element.android.features.startchat.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 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.startchat.impl.addpeople
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.features.startchat.impl.userlist.SelectionMode
|
|
||||||
import io.element.android.features.startchat.impl.userlist.UserListState
|
|
||||||
import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList
|
|
||||||
import io.element.android.features.startchat.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 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.startchat.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.startchat.impl.R
|
|
||||||
import io.element.android.features.startchat.impl.components.UserListView
|
|
||||||
import io.element.android.features.startchat.impl.userlist.UserListEvents
|
|
||||||
import io.element.android.features.startchat.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,
|
|
||||||
onSkipClick: () -> 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 = onSkipClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { 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 = {},
|
|
||||||
onSkipClick = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
|
||||||
|
|
||||||
sealed interface ConfigureRoomEvents {
|
|
||||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
|
||||||
data class TopicChanged(val topic: String) : 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
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
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.startchat.StartChatNavigator
|
|
||||||
import io.element.android.features.startchat.impl.di.CreateRoomScope
|
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
|
||||||
|
|
||||||
@ContributesNode(CreateRoomScope::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<StartChatNavigator>().first()
|
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycle.subscribe(
|
|
||||||
onResume = {
|
|
||||||
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreateRoom))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
val state = presenter.present()
|
|
||||||
ConfigureRoomView(
|
|
||||||
state = state,
|
|
||||||
modifier = modifier,
|
|
||||||
onBackClick = this::navigateUp,
|
|
||||||
onCreateRoomSuccess = {
|
|
||||||
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,209 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
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.startchat.impl.CreateRoomConfig
|
|
||||||
import io.element.android.features.startchat.impl.CreateRoomDataStore
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
|
||||||
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
|
||||||
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
|
|
||||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect
|
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.jvm.optionals.getOrDefault
|
|
||||||
|
|
||||||
class ConfigureRoomPresenter @Inject constructor(
|
|
||||||
private val dataStore: CreateRoomDataStore,
|
|
||||||
private val matrixClient: MatrixClient,
|
|
||||||
private val mediaPickerProvider: PickerProvider,
|
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
|
||||||
private val analyticsService: AnalyticsService,
|
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
|
||||||
private val featureFlagService: FeatureFlagService,
|
|
||||||
private val roomAliasHelper: RoomAliasHelper,
|
|
||||||
) : Presenter<ConfigureRoomState> {
|
|
||||||
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
|
||||||
private var pendingPermissionRequest = false
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun present(): ConfigureRoomState {
|
|
||||||
val cameraPermissionState = cameraPermissionPresenter.present()
|
|
||||||
val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
|
|
||||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
|
||||||
val isKnockFeatureEnabled by remember {
|
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
|
||||||
}.collectAsState(initial = false)
|
|
||||||
val roomAddressValidity = remember {
|
|
||||||
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
|
||||||
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
|
|
||||||
)
|
|
||||||
val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
|
|
||||||
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) }
|
|
||||||
)
|
|
||||||
|
|
||||||
val avatarActions by remember(createRoomConfig.avatarUri) {
|
|
||||||
derivedStateOf {
|
|
||||||
listOfNotNull(
|
|
||||||
AvatarAction.TakePhoto,
|
|
||||||
AvatarAction.ChoosePhoto,
|
|
||||||
AvatarAction.Remove.takeIf { createRoomConfig.avatarUri != null },
|
|
||||||
).toImmutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
|
||||||
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
|
||||||
pendingPermissionRequest = false
|
|
||||||
cameraPhotoPicker.launch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomAddressValidityEffect(
|
|
||||||
client = matrixClient,
|
|
||||||
roomAliasHelper = roomAliasHelper,
|
|
||||||
newRoomAddress = createRoomConfig.roomVisibility.roomAddress().getOrDefault(""),
|
|
||||||
knownRoomAddress = null,
|
|
||||||
) { newRoomAddressValidity ->
|
|
||||||
roomAddressValidity.value = newRoomAddressValidity
|
|
||||||
}
|
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
|
||||||
|
|
||||||
fun createRoom(config: CreateRoomConfig) {
|
|
||||||
createRoomAction.value = AsyncAction.Uninitialized
|
|
||||||
localCoroutineScope.createRoom(config, createRoomAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: ConfigureRoomEvents) {
|
|
||||||
when (event) {
|
|
||||||
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)
|
|
||||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
|
||||||
when (event.action) {
|
|
||||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
|
||||||
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
|
|
||||||
cameraPhotoPicker.launch()
|
|
||||||
} else {
|
|
||||||
pendingPermissionRequest = true
|
|
||||||
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
|
|
||||||
}
|
|
||||||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConfigureRoomState(
|
|
||||||
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
|
||||||
config = createRoomConfig,
|
|
||||||
avatarActions = avatarActions,
|
|
||||||
createRoomAction = createRoomAction.value,
|
|
||||||
cameraPermissionState = cameraPermissionState,
|
|
||||||
homeserverName = homeserverName,
|
|
||||||
roomAddressValidity = roomAddressValidity.value,
|
|
||||||
eventSink = ::handleEvents,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.createRoom(
|
|
||||||
config: CreateRoomConfig,
|
|
||||||
createRoomAction: MutableState<AsyncAction<RoomId>>
|
|
||||||
) = launch {
|
|
||||||
suspend {
|
|
||||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
|
|
||||||
val params = if (config.roomVisibility is RoomVisibilityState.Public) {
|
|
||||||
CreateRoomParameters(
|
|
||||||
name = config.roomName,
|
|
||||||
topic = config.topic,
|
|
||||||
isEncrypted = false,
|
|
||||||
isDirect = false,
|
|
||||||
visibility = RoomVisibility.Public,
|
|
||||||
joinRuleOverride = config.roomVisibility.roomAccess.toJoinRule(),
|
|
||||||
preset = RoomPreset.PUBLIC_CHAT,
|
|
||||||
invite = config.invites.map { it.userId },
|
|
||||||
avatar = avatarUrl,
|
|
||||||
roomAliasName = config.roomVisibility.roomAddress()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
CreateRoomParameters(
|
|
||||||
name = config.roomName,
|
|
||||||
topic = config.topic,
|
|
||||||
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
|
|
||||||
isDirect = false,
|
|
||||||
visibility = RoomVisibility.Private,
|
|
||||||
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
|
||||||
preset = RoomPreset.PRIVATE_CHAT,
|
|
||||||
invite = config.invites.map { it.userId },
|
|
||||||
avatar = avatarUrl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
matrixClient.createRoom(params)
|
|
||||||
.onFailure { failure ->
|
|
||||||
Timber.e(failure, "Failed to create room")
|
|
||||||
}
|
|
||||||
.onSuccess {
|
|
||||||
dataStore.clearCachedData()
|
|
||||||
analyticsService.capture(CreatedRoom(isDM = false))
|
|
||||||
}
|
|
||||||
.getOrThrow()
|
|
||||||
}.runCatchingUpdatingState(createRoomAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun uploadAvatar(avatarUri: Uri): String {
|
|
||||||
val preprocessed = mediaPreProcessor.process(
|
|
||||||
uri = avatarUri,
|
|
||||||
mimeType = MimeTypes.Jpeg,
|
|
||||||
deleteOriginal = false,
|
|
||||||
compressIfPossible = false,
|
|
||||||
).getOrThrow()
|
|
||||||
val byteArray = preprocessed.file.readBytes()
|
|
||||||
return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray, null).getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
|
|
||||||
data class ConfigureRoomPresenterArgs(
|
|
||||||
val selectedUsers: List<MatrixUser>,
|
|
||||||
)
|
|
||||||
|
|
@ -1,30 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import io.element.android.features.startchat.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
|
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
|
||||||
import io.element.android.libraries.permissions.api.PermissionsState
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
|
||||||
val isKnockFeatureEnabled: Boolean,
|
|
||||||
val config: CreateRoomConfig,
|
|
||||||
val avatarActions: ImmutableList<AvatarAction>,
|
|
||||||
val createRoomAction: AsyncAction<RoomId>,
|
|
||||||
val cameraPermissionState: PermissionsState,
|
|
||||||
val roomAddressValidity: RoomAddressValidity,
|
|
||||||
val homeserverName: String,
|
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
|
||||||
) {
|
|
||||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
|
||||||
(config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.features.startchat.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
|
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
|
||||||
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
|
|
||||||
import io.element.android.libraries.permissions.api.PermissionsState
|
|
||||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
|
||||||
override val values: Sequence<ConfigureRoomState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aConfigureRoomState(),
|
|
||||||
aConfigureRoomState(
|
|
||||||
isKnockFeatureEnabled = false,
|
|
||||||
config = CreateRoomConfig(
|
|
||||||
roomName = "Room 101",
|
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
|
||||||
invites = aMatrixUserList().toImmutableList(),
|
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
|
||||||
roomAccess = RoomAccess.Knocking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
aConfigureRoomState(
|
|
||||||
config = CreateRoomConfig(
|
|
||||||
roomName = "Room 101",
|
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
|
||||||
invites = aMatrixUserList().toImmutableList(),
|
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
|
||||||
roomAccess = RoomAccess.Knocking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
aConfigureRoomState(
|
|
||||||
config = CreateRoomConfig(
|
|
||||||
roomName = "Room 101",
|
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
|
||||||
roomAccess = RoomAccess.Knocking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
roomAddressValidity = RoomAddressValidity.NotAvailable,
|
|
||||||
),
|
|
||||||
aConfigureRoomState(
|
|
||||||
config = CreateRoomConfig(
|
|
||||||
roomName = "Room 101",
|
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
|
||||||
roomAccess = RoomAccess.Knocking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
|
|
||||||
),
|
|
||||||
aConfigureRoomState(
|
|
||||||
config = CreateRoomConfig(
|
|
||||||
roomName = "Room 101",
|
|
||||||
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
|
||||||
roomVisibility = RoomVisibilityState.Public(
|
|
||||||
roomAddress = RoomAddress.AutoFilled("Room-101"),
|
|
||||||
roomAccess = RoomAccess.Knocking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
roomAddressValidity = RoomAddressValidity.Valid,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aConfigureRoomState(
|
|
||||||
config: CreateRoomConfig = CreateRoomConfig(),
|
|
||||||
isKnockFeatureEnabled: Boolean = true,
|
|
||||||
avatarActions: List<AvatarAction> = emptyList(),
|
|
||||||
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
|
||||||
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
|
||||||
homeserverName: String = "matrix.org",
|
|
||||||
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
|
|
||||||
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
|
||||||
) = ConfigureRoomState(
|
|
||||||
config = config,
|
|
||||||
isKnockFeatureEnabled = isKnockFeatureEnabled,
|
|
||||||
avatarActions = avatarActions.toImmutableList(),
|
|
||||||
createRoomAction = createRoomAction,
|
|
||||||
cameraPermissionState = cameraPermissionState,
|
|
||||||
homeserverName = homeserverName,
|
|
||||||
roomAddressValidity = roomAddressValidity,
|
|
||||||
eventSink = eventSink,
|
|
||||||
)
|
|
||||||
|
|
@ -1,339 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
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
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.features.startchat.impl.R
|
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
|
|
||||||
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.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
|
||||||
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
|
||||||
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.TextButton
|
|
||||||
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
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ConfigureRoomView(
|
|
||||||
state: ConfigureRoomState,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
onCreateRoomSuccess: (RoomId) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
fun onAvatarClick() {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
isAvatarActionsSheetVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier.clearFocusOnTap(focusManager),
|
|
||||||
topBar = {
|
|
||||||
ConfigureRoomToolbar(
|
|
||||||
isNextActionEnabled = state.isValid,
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
onNextClick = {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
state.eventSink(ConfigureRoomEvents.CreateRoom)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(padding)
|
|
||||||
.imePadding()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.consumeWindowInsets(padding),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
) {
|
|
||||||
RoomNameWithAvatar(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
avatarUri = state.config.avatarUri,
|
|
||||||
roomName = state.config.roomName.orEmpty(),
|
|
||||||
onAvatarClick = ::onAvatarClick,
|
|
||||||
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
|
||||||
)
|
|
||||||
RoomTopic(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
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
|
|
||||||
is RoomVisibilityState.Public -> RoomVisibilityItem.Public
|
|
||||||
},
|
|
||||||
onOptionClick = {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
state.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(it))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (state.config.roomVisibility is RoomVisibilityState.Public && state.isKnockFeatureEnabled) {
|
|
||||||
RoomAccessOptions(
|
|
||||||
selected = when (state.config.roomVisibility.roomAccess) {
|
|
||||||
RoomAccess.Anyone -> RoomAccessItem.Anyone
|
|
||||||
RoomAccess.Knocking -> RoomAccessItem.AskToJoin
|
|
||||||
},
|
|
||||||
onOptionClick = {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
state.eventSink(ConfigureRoomEvents.RoomAccessChanged(it))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
RoomAddressField(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
address = state.config.roomVisibility.roomAddress.value,
|
|
||||||
homeserverName = state.homeserverName,
|
|
||||||
addressValidity = state.roomAddressValidity,
|
|
||||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
|
||||||
label = stringResource(R.string.screen_create_room_room_address_section_title),
|
|
||||||
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
|
|
||||||
)
|
|
||||||
Spacer(Modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AvatarActionBottomSheet(
|
|
||||||
actions = state.avatarActions,
|
|
||||||
isVisible = isAvatarActionsSheetVisible.value,
|
|
||||||
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
|
||||||
onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
|
|
||||||
)
|
|
||||||
|
|
||||||
AsyncActionView(
|
|
||||||
async = state.createRoomAction,
|
|
||||||
progressDialog = {
|
|
||||||
AsyncActionViewDefaults.ProgressDialog(
|
|
||||||
progressText = stringResource(CommonStrings.common_creating_room),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSuccess = { onCreateRoomSuccess(it) },
|
|
||||||
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
|
|
||||||
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
|
|
||||||
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
|
|
||||||
)
|
|
||||||
|
|
||||||
PermissionsView(
|
|
||||||
state = state.cameraPermissionState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun ConfigureRoomToolbar(
|
|
||||||
isNextActionEnabled: Boolean,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
onNextClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
titleStr = stringResource(R.string.screen_create_room_title),
|
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
|
||||||
actions = {
|
|
||||||
TextButton(
|
|
||||||
text = stringResource(CommonStrings.action_create),
|
|
||||||
enabled = isNextActionEnabled,
|
|
||||||
onClick = onNextClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RoomNameWithAvatar(
|
|
||||||
avatarUri: Uri?,
|
|
||||||
roomName: String,
|
|
||||||
onAvatarClick: () -> Unit,
|
|
||||||
onChangeRoomName: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
UnsavedAvatar(
|
|
||||||
avatarUri = avatarUri,
|
|
||||||
avatarSize = AvatarSize.EditRoomDetails,
|
|
||||||
avatarType = AvatarType.Room(),
|
|
||||||
modifier = Modifier.clickable(onClick = onAvatarClick),
|
|
||||||
)
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
label = stringResource(R.string.screen_create_room_room_name_label),
|
|
||||||
value = roomName,
|
|
||||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
|
||||||
singleLine = true,
|
|
||||||
onValueChange = onChangeRoomName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RoomTopic(
|
|
||||||
topic: String,
|
|
||||||
onTopicChange: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
modifier = modifier,
|
|
||||||
label = stringResource(R.string.screen_create_room_topic_label),
|
|
||||||
value = topic,
|
|
||||||
onValueChange = onTopicChange,
|
|
||||||
maxLines = 3,
|
|
||||||
supportingText = stringResource(CommonStrings.common_topic_placeholder),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ConfigureRoomOptions(
|
|
||||||
title: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier.selectableGroup()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
|
||||||
color = ElementTheme.colors.textPrimary,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
)
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RoomVisibilityOptions(
|
|
||||||
selected: RoomVisibilityItem,
|
|
||||||
onOptionClick: (RoomVisibilityItem) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
ConfigureRoomOptions(
|
|
||||||
title = stringResource(R.string.screen_create_room_room_visibility_section_title),
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
RoomVisibilityItem.entries.forEach { item ->
|
|
||||||
val isSelected = item == selected
|
|
||||||
ListItem(
|
|
||||||
leadingContent = ListItemContent.Custom {
|
|
||||||
RoundedIconAtom(
|
|
||||||
size = RoundedIconAtomSize.Big,
|
|
||||||
resourceId = item.icon,
|
|
||||||
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(text = stringResource(item.title)) },
|
|
||||||
supportingContent = { Text(text = stringResource(item.description)) },
|
|
||||||
trailingContent = ListItemContent.RadioButton(selected = isSelected),
|
|
||||||
onClick = { onOptionClick(item) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RoomAccessOptions(
|
|
||||||
selected: RoomAccessItem,
|
|
||||||
onOptionClick: (RoomAccessItem) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
ConfigureRoomOptions(
|
|
||||||
title = stringResource(R.string.screen_create_room_room_access_section_header),
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
RoomAccessItem.entries.forEach { item ->
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(text = stringResource(item.title)) },
|
|
||||||
supportingContent = { Text(text = stringResource(item.description)) },
|
|
||||||
trailingContent = ListItemContent.RadioButton(selected = item == selected),
|
|
||||||
onClick = { onOptionClick(item) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewWithLargeHeight
|
|
||||||
@Composable
|
|
||||||
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
|
||||||
|
|
||||||
@PreviewWithLargeHeight
|
|
||||||
@Composable
|
|
||||||
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
|
||||||
|
|
||||||
@ExcludeFromCoverage
|
|
||||||
@Composable
|
|
||||||
private fun ContentToPreview(state: ConfigureRoomState) {
|
|
||||||
ConfigureRoomView(
|
|
||||||
state = state,
|
|
||||||
onBackClick = {},
|
|
||||||
onCreateRoomSuccess = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
|
||||||
|
|
||||||
enum class RoomAccess {
|
|
||||||
Anyone,
|
|
||||||
Knocking
|
|
||||||
}
|
|
||||||
|
|
||||||
fun RoomAccess.toJoinRule(): JoinRule? {
|
|
||||||
return when (this) {
|
|
||||||
RoomAccess.Anyone -> null
|
|
||||||
RoomAccess.Knocking -> JoinRule.Knock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import io.element.android.features.startchat.impl.R
|
|
||||||
|
|
||||||
enum class RoomAccessItem(
|
|
||||||
@StringRes val title: Int,
|
|
||||||
@StringRes val description: Int
|
|
||||||
) {
|
|
||||||
Anyone(
|
|
||||||
title = R.string.screen_create_room_room_access_section_anyone_option_title,
|
|
||||||
description = R.string.screen_create_room_room_access_section_anyone_option_description,
|
|
||||||
),
|
|
||||||
AskToJoin(
|
|
||||||
title = R.string.screen_create_room_room_access_section_knocking_option_title,
|
|
||||||
description = R.string.screen_create_room_room_access_section_knocking_option_description,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.startchat.impl.configureroom
|
|
||||||
|
|
||||||
sealed class RoomAddress(open val value: String) {
|
|
||||||
data class AutoFilled(override val value: String) : RoomAddress(value)
|
|
||||||
data class Edited(override val value: String) : RoomAddress(value)
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import io.element.android.features.startchat.impl.R
|
|
||||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
|
||||||
|
|
||||||
enum class RoomVisibilityItem(
|
|
||||||
@DrawableRes val icon: Int,
|
|
||||||
@StringRes val title: Int,
|
|
||||||
@StringRes val description: Int
|
|
||||||
) {
|
|
||||||
Private(
|
|
||||||
icon = CompoundDrawables.ic_compound_lock,
|
|
||||||
title = R.string.screen_create_room_private_option_title,
|
|
||||||
description = R.string.screen_create_room_private_option_description,
|
|
||||||
),
|
|
||||||
Public(
|
|
||||||
icon = CompoundDrawables.ic_compound_public,
|
|
||||||
title = R.string.screen_create_room_public_option_title,
|
|
||||||
description = R.string.screen_create_room_public_option_description,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +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.startchat.impl.configureroom
|
|
||||||
|
|
||||||
import java.util.Optional
|
|
||||||
|
|
||||||
sealed interface RoomVisibilityState {
|
|
||||||
data object Private : RoomVisibilityState
|
|
||||||
|
|
||||||
data class Public(
|
|
||||||
val roomAddress: RoomAddress,
|
|
||||||
val roomAccess: RoomAccess,
|
|
||||||
) : RoomVisibilityState
|
|
||||||
|
|
||||||
fun roomAddress(): Optional<String> {
|
|
||||||
return when (this) {
|
|
||||||
is Private -> Optional.empty()
|
|
||||||
is Public -> Optional.of(roomAddress.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.startchat.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.startchat.impl.di
|
|
||||||
|
|
||||||
abstract class CreateRoomScope private constructor()
|
|
||||||
|
|
@ -1,50 +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.startchat.impl.addpeople
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.invitepeople.impl.CreateRoomDataStore
|
|
||||||
import io.element.android.features.startchat.impl.userlist.FakeUserListPresenterFactory
|
|
||||||
import io.element.android.features.invitepeople.impl.userlist.UserListDataStore
|
|
||||||
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
|
|
||||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class AddPeoplePresenterTest {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
private lateinit var presenter: AddPeoplePresenter
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
presenter = AddPeoplePresenter(
|
|
||||||
FakeUserListPresenterFactory(),
|
|
||||||
FakeUserRepository(),
|
|
||||||
CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state`() = runTest {
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
// TODO This doesn't actually test anything...
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.startchat.impl.addpeople
|
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import io.element.android.features.invitepeople.impl.userlist.UserListEvents
|
|
||||||
import io.element.android.features.invitepeople.impl.userlist.UserListState
|
|
||||||
import io.element.android.features.invitepeople.impl.userlist.aUserListState
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
|
||||||
import io.element.android.tests.testutils.clickOn
|
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
|
||||||
import io.element.android.tests.testutils.pressBack
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.rules.TestRule
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class AddPeopleViewTest {
|
|
||||||
@get:Rule
|
|
||||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on back invokes the expected callback`() {
|
|
||||||
val eventsRecorder = EventsRecorder<UserListEvents>()
|
|
||||||
ensureCalledOnce {
|
|
||||||
rule.setAddPeopleView(
|
|
||||||
aUserListState(
|
|
||||||
eventSink = eventsRecorder,
|
|
||||||
),
|
|
||||||
onBackClick = it
|
|
||||||
)
|
|
||||||
rule.pressBack()
|
|
||||||
}
|
|
||||||
eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on back during search emits the expected Event`() {
|
|
||||||
val eventsRecorder = EventsRecorder<UserListEvents>()
|
|
||||||
rule.setAddPeopleView(
|
|
||||||
aUserListState(
|
|
||||||
isSearchActive = true,
|
|
||||||
eventSink = eventsRecorder,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
rule.pressBack()
|
|
||||||
eventsRecorder.assertSingle(UserListEvents.OnSearchActiveChanged(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `clicking on skip invokes the expected callback`() {
|
|
||||||
val eventsRecorder = EventsRecorder<UserListEvents>()
|
|
||||||
ensureCalledOnce {
|
|
||||||
rule.setAddPeopleView(
|
|
||||||
aUserListState(
|
|
||||||
eventSink = eventsRecorder,
|
|
||||||
),
|
|
||||||
onNextClick = it
|
|
||||||
)
|
|
||||||
rule.clickOn(CommonStrings.action_skip)
|
|
||||||
}
|
|
||||||
eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddPeopleView(
|
|
||||||
state: UserListState,
|
|
||||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
|
||||||
onNextClick: () -> Unit = EnsureNeverCalled(),
|
|
||||||
) {
|
|
||||||
setContent {
|
|
||||||
AddPeopleView(
|
|
||||||
state = state,
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
onSkipClick = onNextClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue