feat(security&privacy) : manage save action for edit room address

This commit is contained in:
ganfra 2025-01-22 15:13:36 +01:00
parent 70f39c4f7c
commit 873b6558cc
7 changed files with 101 additions and 10 deletions

View file

@ -9,10 +9,12 @@ package io.element.android.features.roomdetails.impl.securityandprivacy
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
interface SecurityAndPrivacyNavigator : Plugin { interface SecurityAndPrivacyNavigator : Plugin {
fun openEditRoomAddress() fun openEditRoomAddress()
fun closeEditorRoomAddress()
} }
class BackstackSecurityAndPrivacyNavigator( class BackstackSecurityAndPrivacyNavigator(
@ -21,4 +23,8 @@ class BackstackSecurityAndPrivacyNavigator(
override fun openEditRoomAddress() { override fun openEditRoomAddress() {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
} }
override fun closeEditorRoomAddress() {
backStack.pop()
}
} }

View file

@ -9,5 +9,6 @@ package io.element.android.features.roomdetails.impl.securityandprivacy.editroom
sealed interface EditRoomAddressEvents { sealed interface EditRoomAddressEvents {
data object Save : EditRoomAddressEvents data object Save : EditRoomAddressEvents
data object DismissError : EditRoomAddressEvents
data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvents data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvents
} }

View file

@ -12,18 +12,23 @@ 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.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class EditRoomAddressNode @AssistedInject constructor( class EditRoomAddressNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: EditRoomAddressPresenter, presenterFactory: EditRoomAddressPresenter.Factory,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val navigator = plugins<SecurityAndPrivacyNavigator>().first()
private val presenter = presenterFactory.create(navigator)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()

View file

@ -8,27 +8,44 @@
package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.MatrixRoom 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.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.RoomAddressValidity
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEffect 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 client: MatrixClient,
private val room: MatrixRoom, private val room: MatrixRoom,
private val roomAliasHelper: RoomAliasHelper, private val roomAliasHelper: RoomAliasHelper,
) : Presenter<EditRoomAddressState> { ) : Presenter<EditRoomAddressState> {
@AssistedFactory
interface Factory {
fun create(navigator: SecurityAndPrivacyNavigator): EditRoomAddressPresenter
}
@Composable @Composable
override fun present(): EditRoomAddressState { override fun present(): EditRoomAddressState {
val coroutineScope = rememberCoroutineScope()
val homeserverName = remember { client.userIdServerName() } val homeserverName = remember { client.userIdServerName() }
val roomAddressValidity = remember { val roomAddressValidity = remember {
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown) mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
@ -36,7 +53,8 @@ class EditRoomAddressPresenter @Inject constructor(
val savedRoomAddress = remember { val savedRoomAddress = remember {
room.firstAliasMatching(homeserverName)?.roomAddress() room.firstAliasMatching(homeserverName)?.roomAddress()
} }
var currentRoomAddress by remember { val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
var newRoomAddress by remember {
mutableStateOf( mutableStateOf(
savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName) savedRoomAddress ?: roomAliasHelper.roomAliasNameFromRoomDisplayName(room.displayName)
) )
@ -44,9 +62,16 @@ class EditRoomAddressPresenter @Inject constructor(
fun handleEvents(event: EditRoomAddressEvents) { fun handleEvents(event: EditRoomAddressEvents) {
when (event) { when (event) {
EditRoomAddressEvents.Save -> Unit EditRoomAddressEvents.Save -> coroutineScope.save(
saveAction = saveAction,
serverName = homeserverName,
newRoomAddress = newRoomAddress
)
is EditRoomAddressEvents.RoomAddressChanged -> { is EditRoomAddressEvents.RoomAddressChanged -> {
currentRoomAddress = event.roomAddress newRoomAddress = event.roomAddress
}
EditRoomAddressEvents.DismissError -> {
saveAction.value = AsyncAction.Uninitialized
} }
} }
} }
@ -54,7 +79,7 @@ class EditRoomAddressPresenter @Inject constructor(
RoomAddressValidityEffect( RoomAddressValidityEffect(
client = client, client = client,
roomAliasHelper = roomAliasHelper, roomAliasHelper = roomAliasHelper,
newRoomAddress = currentRoomAddress, newRoomAddress = newRoomAddress,
knownRoomAddress = savedRoomAddress knownRoomAddress = savedRoomAddress
) { newRoomAddressValidity -> ) { newRoomAddressValidity ->
roomAddressValidity.value = newRoomAddressValidity roomAddressValidity.value = newRoomAddressValidity
@ -63,11 +88,43 @@ class EditRoomAddressPresenter @Inject constructor(
return EditRoomAddressState( return EditRoomAddressState(
homeserverName = homeserverName, homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity.value, roomAddressValidity = roomAddressValidity.value,
roomAddress = currentRoomAddress, roomAddress = newRoomAddress,
saveAction = saveAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.save(
saveAction: MutableState<AsyncAction<Unit>>,
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? { private fun MatrixRoom.firstAliasMatching(serverName: String): RoomAlias? {

View file

@ -7,12 +7,14 @@
package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress 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 import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
data class EditRoomAddressState( data class EditRoomAddressState(
val homeserverName: String, val homeserverName: String,
val roomAddress: String, val roomAddress: String,
val roomAddressValidity: RoomAddressValidity, val roomAddressValidity: RoomAddressValidity,
val saveAction: AsyncAction<Unit>,
val eventSink: (EditRoomAddressEvents) -> Unit val eventSink: (EditRoomAddressEvents) -> Unit
) { ) {
val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid

View file

@ -8,13 +8,17 @@
package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress package io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
open class EditRoomAddressStateProvider : PreviewParameterProvider<EditRoomAddressState> { open class EditRoomAddressStateProvider : PreviewParameterProvider<EditRoomAddressState> {
override val values: Sequence<EditRoomAddressState> override val values: Sequence<EditRoomAddressState>
get() = sequenceOf( get() = sequenceOf(
aEditRoomAddressState(), 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", roomAddress: String = "therapy",
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown, roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown,
homeserverName: String = ":myserver.org", homeserverName: String = ":myserver.org",
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (EditRoomAddressEvents) -> Unit = {} eventSink: (EditRoomAddressEvents) -> Unit = {}
) = EditRoomAddressState( ) = EditRoomAddressState(
roomAddress = roomAddress, roomAddress = roomAddress,
roomAddressValidity = roomAddressValidity, roomAddressValidity = roomAddressValidity,
homeserverName = homeserverName, homeserverName = homeserverName,
saveAction = saveAction,
eventSink = eventSink eventSink = eventSink
) )

View file

@ -21,6 +21,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme 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.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -70,8 +72,20 @@ fun EditRoomAddressView(
.fillMaxWidth() .fillMaxWidth()
.padding(all = 16.dp) .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) },
)
} }
} }