diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt index 050283e739..8b6fb3312e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/SecurityAndPrivacyNavigator.kt @@ -9,10 +9,12 @@ package io.element.android.features.roomdetails.impl.securityandprivacy import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push interface SecurityAndPrivacyNavigator : Plugin { fun openEditRoomAddress() + fun closeEditorRoomAddress() } class BackstackSecurityAndPrivacyNavigator( @@ -21,4 +23,8 @@ class BackstackSecurityAndPrivacyNavigator( override fun openEditRoomAddress() { backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) } + + override fun closeEditorRoomAddress() { + backStack.pop() + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt index 69b51ce1c6..485a9d2fa0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressEvents.kt @@ -9,5 +9,6 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom sealed interface EditRoomAddressEvents { data object Save : EditRoomAddressEvents + data object DismissError : EditRoomAddressEvents data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt index fbbfee203f..9b1ece00cc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressNode.kt @@ -12,18 +12,23 @@ 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.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) class EditRoomAddressNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: EditRoomAddressPresenter, + presenterFactory: EditRoomAddressPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + private val navigator = plugins().first() + private val presenter = presenterFactory.create(navigator) + @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt index 608c772cb3..31fbaf57ea 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressPresenter.kt @@ -8,27 +8,44 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator +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.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper +import io.element.android.libraries.matrix.api.roomAliasFromName import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect -import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber -class EditRoomAddressPresenter @Inject constructor( +class EditRoomAddressPresenter @AssistedInject constructor( + @Assisted private val navigator: SecurityAndPrivacyNavigator, private val client: MatrixClient, private val room: MatrixRoom, private val roomAliasHelper: RoomAliasHelper, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: SecurityAndPrivacyNavigator): EditRoomAddressPresenter + } @Composable override fun present(): EditRoomAddressState { + val coroutineScope = rememberCoroutineScope() val homeserverName = remember { client.userIdServerName() } val roomAddressValidity = remember { mutableStateOf(RoomAddressValidity.Unknown) @@ -36,7 +53,8 @@ class EditRoomAddressPresenter @Inject constructor( val savedRoomAddress = remember { room.firstAliasMatching(homeserverName)?.roomAddress() } - var currentRoomAddress by remember { + val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + var newRoomAddress by remember { mutableStateOf( savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) ) @@ -44,9 +62,16 @@ class EditRoomAddressPresenter @Inject constructor( fun handleEvents(event: EditRoomAddressEvents) { when (event) { - EditRoomAddressEvents.Save -> Unit + EditRoomAddressEvents.Save -> coroutineScope.save( + saveAction = saveAction, + serverName = homeserverName, + newRoomAddress = newRoomAddress + ) is EditRoomAddressEvents.RoomAddressChanged -> { - currentRoomAddress = event.roomAddress + newRoomAddress = event.roomAddress + } + EditRoomAddressEvents.DismissError -> { + saveAction.value = AsyncAction.Uninitialized } } } @@ -54,7 +79,7 @@ class EditRoomAddressPresenter @Inject constructor( RoomAddressValidityEffect( client = client, roomAliasHelper = roomAliasHelper, - newRoomAddress = currentRoomAddress, + newRoomAddress = newRoomAddress, knownRoomAddress = savedRoomAddress ) { newRoomAddressValidity -> roomAddressValidity.value = newRoomAddressValidity @@ -63,11 +88,43 @@ class EditRoomAddressPresenter @Inject constructor( return EditRoomAddressState( homeserverName = homeserverName, roomAddressValidity = roomAddressValidity.value, - roomAddress = currentRoomAddress, + roomAddress = newRoomAddress, + saveAction = saveAction.value, eventSink = ::handleEvents ) } + private fun CoroutineScope.save( + saveAction: MutableState>, + serverName: String, + newRoomAddress: String, + ) = launch { + suspend { + val savedCanonicalAlias = room.canonicalAlias + val savedAliasFromHomeserver = room.firstAliasMatching(serverName) + val newRoomAlias = client.roomAliasFromName(newRoomAddress).getOrThrow() + + // First publish the new alias in the room directory + room.publishRoomAliasInRoomDirectory(newRoomAlias).getOrThrow() + // Then try remove the old alias from the room directory + if (savedAliasFromHomeserver != null) { + room.removeRoomAliasFromRoomDirectory(savedAliasFromHomeserver).getOrThrow() + } + // Finally update the canonical alias state.. + when { + // Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias + savedCanonicalAlias == null || savedCanonicalAlias.matchesServer(serverName) -> { + room.updateCanonicalAlias(newRoomAlias, room.alternativeAliases).getOrThrow() + } + // Otherwise, update the alternative aliases and keep the current canonical alias + else -> { + val newAlternativeAliases = listOf(newRoomAlias) + room.alternativeAliases + room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow() + } + } + navigator.closeEditorRoomAddress() + }.runCatchingUpdatingState(saveAction) + } } private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt index a4d6dbe5a0..9bc256daba 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressState.kt @@ -7,12 +7,14 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity data class EditRoomAddressState( val homeserverName: String, val roomAddress: String, val roomAddressValidity: RoomAddressValidity, + val saveAction: AsyncAction, val eventSink: (EditRoomAddressEvents) -> Unit ) { val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt index 48d6615615..733e9cb819 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressStateProvider.kt @@ -8,13 +8,17 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity open class EditRoomAddressStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aEditRoomAddressState(), - // Add other states here + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable), + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols), + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid), + aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading), ) } @@ -22,10 +26,12 @@ fun aEditRoomAddressState( roomAddress: String = "therapy", roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown, homeserverName: String = ":myserver.org", + saveAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (EditRoomAddressEvents) -> Unit = {} ) = EditRoomAddressState( roomAddress = roomAddress, roomAddressValidity = roomAddressValidity, homeserverName = homeserverName, + saveAction = saveAction, eventSink = eventSink ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt index c6059dce0c..4c15feea3b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/securityandprivacy/editroomaddress/EditRoomAddressView.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -70,8 +72,20 @@ fun EditRoomAddressView( .fillMaxWidth() .padding(all = 16.dp) ) - } + AsyncActionView( + async = state.saveAction, + progressDialog = { + AsyncActionViewDefaults.ProgressDialog( + progressText = stringResource(CommonStrings.common_saving), + ) + }, + onSuccess = {}, + errorMessage = { stringResource(CommonStrings.error_unknown) }, + onRetry = { state.eventSink(EditRoomAddressEvents.Save) }, + onErrorDismiss = { state.eventSink(EditRoomAddressEvents.DismissError) }, + ) + } }