Browse Source

Merge pull request #324 from vector-im/feature/fre/create_room

Handle create room action
test/jme/compound-poc
Florian Renaud 1 year ago committed by GitHub
parent
commit
13f1ca3a60
  1. 2
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  2. 1
      changelog.d/111.feature
  3. 2
      features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt
  4. 4
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt
  5. 12
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt
  6. 4
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
  7. 15
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt
  8. 43
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
  9. 3
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
  10. 2
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
  11. 29
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
  12. 8
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt
  13. 1
      features/createroom/impl/src/main/res/values/localazy.xml
  14. 60
      features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt
  15. 72
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  16. 7
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  17. 45
      libraries/ui-strings/src/main/res/values/localazy.xml

2
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -199,7 +199,7 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
NavTarget.CreateRoom -> { NavTarget.CreateRoom -> {
val callback = object : CreateRoomEntryPoint.Callback { val callback = object : CreateRoomEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) { override fun onSuccess(roomId: RoomId) {
backstack.replace(NavTarget.Room(roomId)) backstack.replace(NavTarget.Room(roomId))
} }
} }

1
changelog.d/111.feature

@ -0,0 +1 @@
[Create and join rooms] Create a room and show it

2
features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt

@ -31,6 +31,6 @@ interface CreateRoomEntryPoint : FeatureEntryPoint {
} }
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId) fun onSuccess(roomId: RoomId)
} }
} }

4
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt

@ -76,10 +76,10 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
backstack.push(NavTarget.ConfigureRoom) backstack.push(NavTarget.ConfigureRoom)
} }
} }
createNode<AddPeopleNode>(context = buildContext, plugins = listOf(callback)) createNode<AddPeopleNode>(context = buildContext, plugins = plugins.plus(callback))
} }
NavTarget.ConfigureRoom -> { NavTarget.ConfigureRoom -> {
createNode<ConfigureRoomNode>(context = buildContext) createNode<ConfigureRoomNode>(context = buildContext, plugins = plugins)
} }
} }
} }

12
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt

@ -30,6 +30,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@ -67,14 +68,19 @@ class CreateRoomFlowNode @AssistedInject constructor(
backstack.push(NavTarget.NewRoom) backstack.push(NavTarget.NewRoom)
} }
override fun onOpenRoom(roomId: RoomId) { override fun onStartChatSuccess(roomId: RoomId) {
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) } plugins<CreateRoomEntryPoint.Callback>().forEach { it.onSuccess(roomId) }
} }
} }
createNode<CreateRoomRootNode>(context = buildContext, plugins = listOf(callback)) createNode<CreateRoomRootNode>(context = buildContext, plugins = listOf(callback))
} }
NavTarget.NewRoom -> { NavTarget.NewRoom -> {
createNode<ConfigureRoomFlowNode>(context = buildContext, plugins = emptyList()) val callback = object : ConfigureRoomNode.Callback {
override fun onCreateRoomSuccess(roomId: RoomId) {
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onSuccess(roomId) }
}
}
createNode<ConfigureRoomFlowNode>(context = buildContext, plugins = listOf(callback))
} }
} }
} }

4
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt

@ -17,6 +17,7 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import android.net.Uri import android.net.Uri
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser
sealed interface ConfigureRoomEvents { sealed interface ConfigureRoomEvents {
@ -25,5 +26,6 @@ sealed interface ConfigureRoomEvents {
data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents
data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
object CreateRoom : ConfigureRoomEvents data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
object CancelCreateRoom : ConfigureRoomEvents
} }

15
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt

@ -21,10 +21,12 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(CreateRoomScope::class) @ContributesNode(CreateRoomScope::class)
class ConfigureRoomNode @AssistedInject constructor( class ConfigureRoomNode @AssistedInject constructor(
@ -33,13 +35,24 @@ class ConfigureRoomNode @AssistedInject constructor(
private val presenter: ConfigureRoomPresenter, private val presenter: ConfigureRoomPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onCreateRoomSuccess(roomId: RoomId)
}
private val callback = object : Callback {
override fun onCreateRoomSuccess(roomId: RoomId) {
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
ConfigureRoomView( ConfigureRoomView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackPressed = { navigateUp() } // TODO we should keep in memory the current view state onBackPressed = this::navigateUp,
onRoomCreated = callback::onCreateRoomSuccess
) )
} }
} }

43
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt

@ -17,17 +17,30 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
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.createroom.RoomVisibility
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class ConfigureRoomPresenter @Inject constructor( class ConfigureRoomPresenter @Inject constructor(
private val dataStore: CreateRoomDataStore, private val dataStore: CreateRoomDataStore,
private val matrixClient: MatrixClient,
) : Presenter<ConfigureRoomState> { ) : Presenter<ConfigureRoomState> {
@Composable @Composable
@ -39,6 +52,14 @@ class ConfigureRoomPresenter @Inject constructor(
} }
} }
val localCoroutineScope = rememberCoroutineScope()
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
fun createRoom(config: CreateRoomConfig) {
createRoomAction.value = Async.Uninitialized
localCoroutineScope.createRoom(config, createRoomAction)
}
fun handleEvents(event: ConfigureRoomEvents) { fun handleEvents(event: ConfigureRoomEvents) {
when (event) { when (event) {
is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString()) is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString())
@ -46,14 +67,32 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
ConfigureRoomEvents.CreateRoom -> Unit // TODO is ConfigureRoomEvents.CreateRoom -> createRoom(event.config)
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = Async.Uninitialized
} }
} }
return ConfigureRoomState( return ConfigureRoomState(
createRoomConfig.value, config = createRoomConfig.value,
isCreateButtonEnabled = isCreateButtonEnabled, isCreateButtonEnabled = isCreateButtonEnabled,
createRoomAction = createRoomAction.value,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
private fun CoroutineScope.createRoom(config: CreateRoomConfig, createRoomAction: MutableState<Async<RoomId>>) = launch {
suspend {
val params = CreateRoomParameters(
name = config.roomName,
topic = config.topic,
isEncrypted = config.privacy == RoomPrivacy.Private,
isDirect = false,
visibility = if (config.privacy == RoomPrivacy.Public) RoomVisibility.PUBLIC else RoomVisibility.PRIVATE,
preset = if (config.privacy == RoomPrivacy.Public) RoomPreset.PUBLIC_CHAT else RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.id },
avatar = config.avatarUrl,
)
matrixClient.createRoom(params).getOrThrow()
}.execute(createRoomAction)
}
} }

3
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt

@ -17,9 +17,12 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
data class ConfigureRoomState( data class ConfigureRoomState(
val config: CreateRoomConfig, val config: CreateRoomConfig,
val isCreateButtonEnabled: Boolean, val isCreateButtonEnabled: Boolean,
val createRoomAction: Async<RoomId>,
val eventSink: (ConfigureRoomEvents) -> Unit val eventSink: (ConfigureRoomEvents) -> Unit
) )

2
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt

@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.configureroom
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.features.userlist.api.aListOfSelectedUsers
import io.element.android.libraries.architecture.Async
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> { open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
override val values: Sequence<ConfigureRoomState> override val values: Sequence<ConfigureRoomState>
@ -39,5 +40,6 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
fun aConfigureRoomState() = ConfigureRoomState( fun aConfigureRoomState() = ConfigureRoomState(
config = CreateRoomConfig(), config = CreateRoomConfig(),
isCreateButtonEnabled = false, isCreateButtonEnabled = false,
createRoomAction = Async.Uninitialized,
eventSink = {} eventSink = {}
) )

29
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt

@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -44,13 +45,17 @@ import io.element.android.features.createroom.impl.components.Avatar
import io.element.android.features.createroom.impl.components.LabelledTextField import io.element.android.features.createroom.impl.components.LabelledTextField
import io.element.android.features.createroom.impl.components.RoomPrivacyOption import io.element.android.features.createroom.impl.components.RoomPrivacyOption
import io.element.android.features.userlist.api.components.SelectedUsersList import io.element.android.features.userlist.api.components.SelectedUsersList
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
import io.element.android.libraries.designsystem.theme.components.Scaffold 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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.R as StringR import io.element.android.libraries.ui.strings.R as StringR
@Composable @Composable
@ -58,7 +63,14 @@ fun ConfigureRoomView(
state: ConfigureRoomState, state: ConfigureRoomState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {}, onBackPressed: () -> Unit = {},
onRoomCreated: (RoomId) -> Unit = {},
) { ) {
if (state.createRoomAction is Async.Success) {
LaunchedEffect(state.createRoomAction) {
onRoomCreated(state.createRoomAction.state)
}
}
val context = LocalContext.current val context = LocalContext.current
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
@ -67,8 +79,7 @@ fun ConfigureRoomView(
isNextActionEnabled = state.isCreateButtonEnabled, isNextActionEnabled = state.isCreateButtonEnabled,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
onNextPressed = { onNextPressed = {
// state.eventSink(ConfigureRoomEvents.CreateRoom) state.eventSink(ConfigureRoomEvents.CreateRoom(state.config))
Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show()
}, },
) )
} }
@ -102,6 +113,20 @@ fun ConfigureRoomView(
) )
} }
} }
when (state.createRoomAction) {
is Async.Loading -> {
ProgressDialog(text = stringResource(StringR.string.common_creating_room))
}
is Async.Failure -> {
RetryDialog(
content = stringResource(R.string.screen_create_room_error_creating_room),
onDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
)
}
else -> Unit
}
} }
@Composable @Composable

8
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootNode.kt

@ -37,7 +37,7 @@ class CreateRoomRootNode @AssistedInject constructor(
interface Callback : Plugin { interface Callback : Plugin {
fun onCreateNewRoom() fun onCreateNewRoom()
fun onOpenRoom(roomId: RoomId) fun onStartChatSuccess(roomId: RoomId)
} }
private val callback = object : Callback { private val callback = object : Callback {
@ -45,8 +45,8 @@ class CreateRoomRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onCreateNewRoom() } plugins<Callback>().forEach { it.onCreateNewRoom() }
} }
override fun onOpenRoom(roomId: RoomId) { override fun onStartChatSuccess(roomId: RoomId) {
plugins<Callback>().forEach { it.onOpenRoom(roomId) } plugins<Callback>().forEach { it.onStartChatSuccess(roomId) }
} }
} }
@ -58,7 +58,7 @@ class CreateRoomRootNode @AssistedInject constructor(
modifier = modifier, modifier = modifier,
onClosePressed = this::navigateUp, onClosePressed = this::navigateUp,
onNewRoomClicked = callback::onCreateNewRoom, onNewRoomClicked = callback::onCreateNewRoom,
onOpenDM = callback::onOpenRoom, onOpenDM = callback::onStartChatSuccess,
) )
} }
} }

1
features/createroom/impl/src/main/res/values/localazy.xml

@ -3,6 +3,7 @@
<string name="screen_create_room_action_create_room">"New room"</string> <string name="screen_create_room_action_create_room">"New room"</string>
<string name="screen_create_room_action_invite_people">"Invite people"</string> <string name="screen_create_room_action_invite_people">"Invite people"</string>
<string name="screen_create_room_add_people_title">"Add people"</string> <string name="screen_create_room_add_people_title">"Add people"</string>
<string name="screen_create_room_error_creating_room">"An error occurred when creating the room"</string>
<string name="screen_create_room_private_option_description">"Messages in this room are encrypted. Encryption can’t be disabled afterwards."</string> <string name="screen_create_room_private_option_description">"Messages in this room are encrypted. Encryption can’t be disabled afterwards."</string>
<string name="screen_create_room_private_option_title">"Private room (invite only)"</string> <string name="screen_create_room_private_option_title">"Private room (invite only)"</string>
<string name="screen_create_room_public_option_description">"Messages are not encrypted and anyone can read them. You can enable encryption at a later date."</string> <string name="screen_create_room_public_option_description">"Messages are not encrypted and anyone can read them. You can enable encryption at a later date."</string>

60
features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt

@ -26,9 +26,13 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
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.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -44,11 +48,16 @@ class ConfigureRoomPresenterTests {
private lateinit var presenter: ConfigureRoomPresenter private lateinit var presenter: ConfigureRoomPresenter
private lateinit var userListDataStore: UserListDataStore private lateinit var userListDataStore: UserListDataStore
private lateinit var fakeMatrixClient: FakeMatrixClient
@Before @Before
fun setup() { fun setup() {
fakeMatrixClient = FakeMatrixClient()
userListDataStore = UserListDataStore() userListDataStore = UserListDataStore()
presenter = ConfigureRoomPresenter(CreateRoomDataStore(userListDataStore)) presenter = ConfigureRoomPresenter(
dataStore = CreateRoomDataStore(userListDataStore),
matrixClient = fakeMatrixClient
)
} }
@Test @Test
@ -149,5 +158,54 @@ class ConfigureRoomPresenterTests {
assertThat(newState.config).isEqualTo(expectedConfig) assertThat(newState.config).isEqualTo(expectedConfig)
} }
} }
@Test
fun `present - trigger create room action`() = runTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val createRoomResult = Result.success(RoomId("!createRoomResult"))
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Success::class.java)
assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
}
}
@Test
fun `present - trigger retry and cancel actions`() = runTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val createRoomResult = Result.failure<RoomId>(A_THROWABLE)
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
// Create
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
val stateAfterRetry = awaitItem()
assertThat(stateAfterRetry.createRoomAction).isInstanceOf(Async.Failure::class.java)
assertThat((stateAfterRetry.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Cancel
stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
}
}
} }

72
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.asRoomId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters 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.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.createroom.RoomVisibility
@ -40,9 +41,12 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.RequiredState
@ -180,43 +184,44 @@ class RustMatrixClient constructor(
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = withContext(dispatchers.io) { override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = withContext(dispatchers.io) {
runCatching { runCatching {
val roomId = client.createRoom( val rustParams = RustCreateRoomParameters(
RustCreateRoomParameters( name = createRoomParams.name,
name = createRoomParams.name, topic = createRoomParams.topic,
topic = createRoomParams.topic, isEncrypted = createRoomParams.isEncrypted,
isEncrypted = createRoomParams.isEncrypted, isDirect = createRoomParams.isDirect,
isDirect = createRoomParams.isDirect, visibility = when (createRoomParams.visibility) {
visibility = when (createRoomParams.visibility) { RoomVisibility.PUBLIC -> RustRoomVisibility.PUBLIC
RoomVisibility.PUBLIC -> RustRoomVisibility.PUBLIC RoomVisibility.PRIVATE -> RustRoomVisibility.PRIVATE
RoomVisibility.PRIVATE -> RustRoomVisibility.PRIVATE },
}, preset = when (createRoomParams.preset) {
preset = when (createRoomParams.preset) { RoomPreset.PRIVATE_CHAT -> RustRoomPreset.PRIVATE_CHAT
RoomPreset.PRIVATE_CHAT -> RustRoomPreset.PRIVATE_CHAT RoomPreset.PUBLIC_CHAT -> RustRoomPreset.PUBLIC_CHAT
RoomPreset.PUBLIC_CHAT -> RustRoomPreset.PUBLIC_CHAT RoomPreset.TRUSTED_PRIVATE_CHAT -> RustRoomPreset.TRUSTED_PRIVATE_CHAT
RoomPreset.TRUSTED_PRIVATE_CHAT -> RustRoomPreset.TRUSTED_PRIVATE_CHAT },
}, invite = createRoomParams.invite?.map { it.value },
invite = createRoomParams.invite?.map { it.value }, avatar = createRoomParams.avatar,
avatar = createRoomParams.avatar,
)
) )
RoomId(roomId) val roomId = client.createRoom(rustParams).asRoomId()
// Wait to receive the room back from the sync
withTimeout(30_000L) {
slidingSyncObserverProxy.updateSummaryFlow.filter { roomId.value in it.rooms }.first()
}
roomId
} }
} }
override suspend fun createDM(userId: UserId): Result<RoomId> = withContext(dispatchers.io) { override suspend fun createDM(userId: UserId): Result<RoomId> {
runCatching { val createRoomParams = CreateRoomParameters(
val roomId = client.createRoom( name = null,
RustCreateRoomParameters( isEncrypted = true,
name = null, isDirect = true,
isEncrypted = true, visibility = RoomVisibility.PRIVATE,
isDirect = true, preset = RoomPreset.TRUSTED_PRIVATE_CHAT,
visibility = RustRoomVisibility.PRIVATE, invite = listOf(userId)
preset = RustRoomPreset.TRUSTED_PRIVATE_CHAT, )
invite = listOf(userId.value), return createRoom(createRoomParams)
)
)
RoomId(roomId)
}
} }
override fun mediaResolver(): MediaResolver = mediaResolver override fun mediaResolver(): MediaResolver = mediaResolver
@ -321,3 +326,4 @@ class RustMatrixClient constructor(
return sessionDirectory.deleteRecursively() return sessionDirectory.deleteRecursively()
} }
} }

7
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -46,6 +46,7 @@ class FakeMatrixClient(
private val notificationService: FakeNotificationService = FakeNotificationService(), private val notificationService: FakeNotificationService = FakeNotificationService(),
) : MatrixClient { ) : MatrixClient {
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID) private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmFailure: Throwable? = null private var createDmFailure: Throwable? = null
private var findDmResult: MatrixRoom? = FakeMatrixRoom() private var findDmResult: MatrixRoom? = FakeMatrixRoom()
@ -61,7 +62,7 @@ class FakeMatrixClient(
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> { override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> {
delay(100) delay(100)
return Result.success(A_ROOM_ID) return createRoomResult
} }
override suspend fun createDM(userId: UserId): Result<RoomId> { override suspend fun createDM(userId: UserId): Result<RoomId> {
@ -119,6 +120,10 @@ class FakeMatrixClient(
logoutFailure = failure logoutFailure = failure
} }
fun givenCreateRoomResult(result: Result<RoomId>) {
createRoomResult = result
}
fun givenCreateDmResult(result: Result<RoomId>) { fun givenCreateDmResult(result: Result<RoomId>) {
createDmResult = result createDmResult = result
} }

45
libraries/ui-strings/src/main/res/values/localazy.xml

@ -121,60 +121,15 @@
<string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite."</string> <string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite."</string>
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string> <string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string> <string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="notification_channel_call">"Call"</string>
<string name="notification_channel_listening_for_events">"Listening for events"</string>
<string name="notification_channel_noisy">"Noisy notifications"</string>
<string name="notification_channel_silent">"Silent notifications"</string>
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
<string name="notification_invitation_action_join">"Join"</string>
<string name="notification_invitation_action_reject">"Reject"</string>
<string name="notification_new_messages">"New Messages"</string>
<string name="notification_room_action_mark_as_read">"Mark as read"</string>
<string name="notification_room_action_quick_reply">"Quick reply"</string>
<string name="notification_sender_me">"Me"</string>
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
<string name="notification_unread_notified_messages_and_invitation">"%1$s and %2$s"</string>
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string>
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s and %3$s"</string>
<plurals name="common_member_count"> <plurals name="common_member_count">
<item quantity="one">"%1$d member"</item> <item quantity="one">"%1$d member"</item>
<item quantity="other">"%1$d members"</item> <item quantity="other">"%1$d members"</item>
</plurals> </plurals>
<plurals name="notification_compat_summary_line_for_room">
<item quantity="one">"%1$s: %2$d message"</item>
<item quantity="other">"%1$s: %2$d messages"</item>
</plurals>
<plurals name="notification_compat_summary_title">
<item quantity="one">"%d notification"</item>
<item quantity="other">"%d notifications"</item>
</plurals>
<plurals name="notification_invitations">
<item quantity="one">"%d invitation"</item>
<item quantity="other">"%d invitations"</item>
</plurals>
<plurals name="notification_new_messages_for_room">
<item quantity="one">"%d new message"</item>
<item quantity="other">"%d new messages"</item>
</plurals>
<plurals name="notification_unread_notified_messages">
<item quantity="one">"%d unread notified message"</item>
<item quantity="other">"%d unread notified messages"</item>
</plurals>
<plurals name="notification_unread_notified_messages_in_room_rooms">
<item quantity="one">"%d room"</item>
<item quantity="other">"%d rooms"</item>
</plurals>
<plurals name="room_timeline_state_changes"> <plurals name="room_timeline_state_changes">
<item quantity="one">"%1$d room change"</item> <item quantity="one">"%1$d room change"</item>
<item quantity="other">"%1$d room changes"</item> <item quantity="other">"%1$d room changes"</item>
</plurals> </plurals>
<string name="preference_rageshake">"Rageshake to report bug"</string> <string name="preference_rageshake">"Rageshake to report bug"</string>
<string name="push_choose_distributor_dialog_title_android">"Choose how to receive notifications"</string>
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
<string name="push_distributor_firebase_android">"Google Services"</string>
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string> <string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
<string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string> <string name="report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
<string name="report_content_hint">"Reason for reporting this content"</string> <string name="report_content_hint">"Reason for reporting this content"</string>

Loading…
Cancel
Save