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.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()
}
}

View File

@@ -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
}

View File

@@ -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<Plugin>,
private val presenter: EditRoomAddressPresenter,
presenterFactory: EditRoomAddressPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
private val navigator = plugins<SecurityAndPrivacyNavigator>().first()
private val presenter = presenterFactory.create(navigator)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()

View File

@@ -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<EditRoomAddressState> {
@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>(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<Unit>>(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<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? {

View File

@@ -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<Unit>,
val eventSink: (EditRoomAddressEvents) -> Unit
) {
val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid

View File

@@ -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<EditRoomAddressState> {
override val values: Sequence<EditRoomAddressState>
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<Unit> = AsyncAction.Uninitialized,
eventSink: (EditRoomAddressEvents) -> Unit = {}
) = EditRoomAddressState(
roomAddress = roomAddress,
roomAddressValidity = roomAddressValidity,
homeserverName = homeserverName,
saveAction = saveAction,
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.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) },
)
}
}