diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 8143114573..21d8f8d13f 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -30,7 +30,7 @@ import io.element.android.features.createroom.impl.CreateRoomConfig 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.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -131,7 +131,7 @@ class ConfigureRoomPresenter @Inject constructor( dataStore.clearCachedData() analyticsService.capture(CreatedRoom(isDM = false)) } - }.execute(createRoomAction) + }.runCatchingUpdatingState(createRoomAction) } private suspend fun uploadAvatar(avatarUri: Uri): String { diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 703fa268e2..c66dbc1e59 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -84,7 +84,7 @@ fun ConfigureRoomView( if (state.createRoomAction is Async.Success) { LaunchedEffect(state.createRoomAction) { - onRoomCreated(state.createRoomAction.state) + onRoomCreated(state.createRoomAction.data) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 70f006f643..67bf22d22a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -28,7 +28,7 @@ import io.element.android.features.createroom.impl.userlist.UserListPresenter import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -95,6 +95,6 @@ class CreateRoomRootPresenter @Inject constructor( suspend { matrixClient.createDM(user.userId).getOrThrow() .also { analyticsService.capture(CreatedRoom(isDM = true)) } - }.execute(startDmAction) + }.runCatchingUpdatingState(startDmAction) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 1265243c50..c83c0e620b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -69,7 +69,7 @@ fun CreateRoomRootView( ) { if (state.startDmAction is Async.Success) { LaunchedEffect(state.startDmAction) { - onOpenDM(state.startDmAction.state) + onOpenDM(state.startDmAction.data) } } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt index db9fec3153..d537517d54 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt @@ -30,7 +30,7 @@ import io.element.android.features.invitelist.impl.model.InviteListInviteSummary import io.element.android.features.invitelist.impl.model.InviteSender import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -135,7 +135,7 @@ class InviteListPresenter @Inject constructor( it.acceptInvitation().getOrThrow() } roomId - }.execute(acceptedAction) + }.runCatchingUpdatingState(acceptedAction) } private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = launch { @@ -143,7 +143,7 @@ class InviteListPresenter @Inject constructor( client.getRoom(roomId)?.use { it.rejectInvitation().getOrThrow() } ?: Unit - }.execute(declinedAction) + }.runCatchingUpdatingState(declinedAction) } private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run { diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt index 5ef0402055..70e4041c35 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt @@ -58,7 +58,7 @@ fun InviteListView( ) { if (state.acceptedAction is Async.Success) { LaunchedEffect(state.acceptedAction) { - onInviteAccepted(state.acceptedAction.state) + onInviteAccepted(state.acceptedAction.data) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 2e3f9548d6..ef9f9e2441 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -26,7 +26,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import kotlinx.coroutines.CoroutineScope @@ -71,6 +71,6 @@ class ChangeServerPresenter @Inject constructor( // Valid, remember user choice accountProviderDataSource.userSelection(data) }.getOrThrow() - }.execute(changeServerAction, errorMapping = ChangeServerError::from) + }.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 2626e56365..123d3013c7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -30,7 +30,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import kotlinx.coroutines.CoroutineScope @@ -95,6 +95,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( throw IllegalStateException("Unsupported login flow") } }.getOrThrow() - }.execute(loginFlowAction, errorMapping = ChangeServerError::from) + }.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index e6e1ce8e83..3efa0be668 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -131,7 +131,7 @@ fun ConfirmAccountProviderView( } is Async.Loading -> Unit // The Continue button shows the loading state is Async.Success -> { - when (val loginFlowState = state.loginFlow.state) { + when (val loginFlowState = state.loginFlow.data) { is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails) LoginFlow.PasswordLogin -> onLoginPasswordNeeded() } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index 5c280cba0e..3f63f36134 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -171,7 +171,7 @@ fun SearchAccountProviderView( } } is Async.Success -> { - items(state.userInputResult.state) { homeserverData -> + items(state.userInputResult.data) { homeserverData -> val item = homeserverData.toAccountProvider() AccountProviderView( item = item, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt index baad764025..e957755b98 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt @@ -26,7 +26,7 @@ import io.element.android.features.logout.api.LogoutPreferenceEvents import io.element.android.features.logout.api.LogoutPreferencePresenter import io.element.android.features.logout.api.LogoutPreferenceState import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope @@ -59,6 +59,6 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli private fun CoroutineScope.logout(logoutAction: MutableState>) = launch { suspend { matrixClient.logout() - }.execute(logoutAction) + }.runCatchingUpdatingState(logoutAction) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 807a23f6cc..9761a939b3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.executeResult +import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.mediaupload.api.MediaSender import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -83,8 +83,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( mediaAttachment: Attachment.Media, sendActionState: MutableState>, ) { - suspend { + sendActionState.runUpdatingState { mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible) - }.executeResult(sendActionState) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt index 17494f892e..38ab84367a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -24,14 +24,12 @@ 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 androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index 5ef1bdcd47..d0c0abfbca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -117,7 +117,7 @@ class MediaViewerPresenter @AssistedInject constructor( private fun CoroutineScope.saveOnDisk(localMedia: Async) = launch { if (localMedia is Async.Success) { - localMediaActions.saveOnDisk(localMedia.state) + localMediaActions.saveOnDisk(localMedia.data) .onSuccess { val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android) snackbarDispatcher.post(snackbarMessage) @@ -131,7 +131,7 @@ class MediaViewerPresenter @AssistedInject constructor( private fun CoroutineScope.share(localMedia: Async) = launch { if (localMedia is Async.Success) { - localMediaActions.share(localMedia.state) + localMediaActions.share(localMedia.data) .onFailure { val snackbarMessage = SnackbarMessage(mediaActionsError(it)) snackbarDispatcher.post(snackbarMessage) @@ -141,7 +141,7 @@ class MediaViewerPresenter @AssistedInject constructor( private fun CoroutineScope.open(localMedia: Async) = launch { if (localMedia is Async.Success) { - localMediaActions.open(localMedia.state) + localMediaActions.open(localMedia.data) .onFailure { val snackbarMessage = SnackbarMessage(mediaActionsError(it)) snackbarDispatcher.post(snackbarMessage) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 9cc1ed69fd..1f540f4c38 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -57,7 +57,6 @@ import io.element.android.features.messages.impl.media.local.LocalMediaView import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.RetryDialog diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt index 09cad8e33b..c8476a9b64 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt @@ -29,8 +29,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.executeResult -import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.core.EventId @@ -87,12 +86,12 @@ class ReportMessagePresenter @AssistedInject constructor( blockUser: Boolean, result: MutableState>, ) = launch { - suspend { + result.runUpdatingState { val userIdToBlock = userId.takeIf { blockUser } room.reportContent(eventId, reason, userIdToBlock) .onSuccess { snackbarDispatcher.post(SnackbarMessage(StringR.string.common_report_submitted)) } - }.executeResult(result) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index d4430dd2e3..2e10622c7a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -29,7 +29,7 @@ import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -128,13 +128,13 @@ class DeveloperSettingsPresenter @Inject constructor( private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch { suspend { computeCacheSizeUseCase() - }.execute(cacheSize) + }.runCatchingUpdatingState(cacheSize) } private fun CoroutineScope.clearCache(clearCacheAction: MutableState>) = launch { suspend { clearCacheUseCase() - }.execute(clearCacheAction) + }.runCatchingUpdatingState(clearCacheAction) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 027b3cfd1d..3c1bd1f9ba 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -23,8 +23,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.components.preferences.PreferenceView diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt index 615541a3de..d4d6643038 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.setValue import androidx.core.net.toUri import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.StateEventType @@ -154,7 +154,7 @@ class RoomDetailsEditPresenter @Inject constructor( }) } if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow() - }.execute(action) + }.runCatchingUpdatingState(action) } private suspend fun updateAvatar(avatarUri: Uri?): Result { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt index ef882c2f40..00cb04b118 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.RoomMember @@ -147,7 +147,7 @@ class RoomInviteMembersPresenter @Inject constructor( withContext(coroutineDispatchers.io) { roomMemberListDataSource.search("").toImmutableList() } - }.execute(roomMembers) + }.runCatchingUpdatingState(roomMembers) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 7e87062481..41805bdb77 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton @@ -110,7 +109,7 @@ fun RoomMemberListView( if (!state.isSearchActive) { if (state.roomMembers is Async.Success) { RoomMemberList( - roomMembers = state.roomMembers.state, + roomMembers = state.roomMembers.data, showMembersCount = true, onUserSelected = ::onUserSelected ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 01f60847f8..c3a79481e6 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -55,8 +55,8 @@ class RoomMemberListPresenterTests { val loadedState = awaitItem() Truth.assertThat(loadedState.roomMembers).isInstanceOf(Async.Success::class.java) - Truth.assertThat((loadedState.roomMembers as Async.Success).state.invited).isEqualTo(listOf(aVictor(), aWalter())) - Truth.assertThat((loadedState.roomMembers as Async.Success).state.joined).isNotEmpty() + Truth.assertThat((loadedState.roomMembers as Async.Success).data.invited).isEqualTo(listOf(aVictor(), aWalter())) + Truth.assertThat((loadedState.roomMembers as Async.Success).data.joined).isNotEmpty() } } diff --git a/libraries/architecture/build.gradle.kts b/libraries/architecture/build.gradle.kts index 5d33a08d2d..68a25ead04 100644 --- a/libraries/architecture/build.gradle.kts +++ b/libraries/architecture/build.gradle.kts @@ -26,4 +26,8 @@ dependencies { api(libs.dagger) api(libs.appyx.core) api(libs.androidx.lifecycle.runtime) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.truth) } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index 301dcfd936..fe728562e9 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -18,51 +18,152 @@ package io.element.android.libraries.architecture import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +/** + * Sealed type that allows to model an asynchronous operation. + */ @Stable sealed interface Async { + + /** + * Represents a failed operation. + * + * @param T the type of data returned by the operation. + * @property error the error that caused the operation to fail. + * @property prevData the data returned by a previous successful run of the operation if any. + */ + data class Failure( + val error: Throwable, + val prevData: T? = null, + ) : Async + + /** + * Represents an operation that is currently ongoing. + * + * @param T the type of data returned by the operation. + * @property prevData the data returned by a previous successful run of the operation if any. + */ + data class Loading( + val prevData: T? = null, + ) : Async + + /** + * Represents a successful operation. + * + * @param T the type of data returned by the operation. + * @property data the data returned by the operation. + */ + data class Success( + val data: T, + ) : Async + + /** + * Represents an uninitialized operation (i.e. yet to be run). + */ object Uninitialized : Async - data class Loading(val prevState: T? = null) : Async - data class Failure(val error: Throwable, val prevState: T? = null) : Async - data class Success(val state: T) : Async - fun dataOrNull(): T? { - return when (this) { - is Failure -> prevState - is Loading -> prevState - is Success -> state - Uninitialized -> null + /** + * Returns the data returned by the operation, or null otherwise. + * + * Please note this method may return stale data if the operation is not [Success]. + */ + fun dataOrNull(): T? = when (this) { + is Failure -> prevData + is Loading -> prevData + is Success -> data + Uninitialized -> null + } + + /** + * Returns the error that caused the operation to fail, or null otherwise. + */ + fun errorOrNull(): Throwable? = when (this) { + is Failure -> error + else -> null + } + + fun isFailure(): Boolean = this is Failure + + fun isLoading(): Boolean = this is Loading + + fun isSuccess(): Boolean = this is Success + + fun isUninitialized(): Boolean = this == Uninitialized +} + +suspend inline fun MutableState>.runCatchingUpdatingState( + errorTransform: (Throwable) -> Throwable = { it }, + block: () -> T, +): Result = runUpdatingState( + state = this, + errorTransform = errorTransform, + resultBlock = { + runCatching { + block() } - } -} + }, +) -suspend inline fun (suspend () -> T).execute( +suspend inline fun (suspend () -> T).runCatchingUpdatingState( state: MutableState>, - errorMapping: ((Throwable) -> Throwable) = { it }, -) { - try { - state.value = Async.Loading() - val result = this() - state.value = Async.Success(result) - } catch (error: Throwable) { - state.value = Async.Failure(errorMapping.invoke(error)) - } -} + errorTransform: (Throwable) -> Throwable = { it }, +): Result = runUpdatingState( + state = state, + errorTransform = errorTransform, + resultBlock = { + runCatching { + this() + } + }, +) -suspend inline fun (suspend () -> Result).executeResult(state: MutableState>) { - if (state.value !is Async.Success) { - state.value = Async.Loading() +suspend inline fun MutableState>.runUpdatingState( + errorTransform: (Throwable) -> Throwable = { it }, + resultBlock: () -> Result, +): Result = runUpdatingState( + state = this, + errorTransform = errorTransform, + resultBlock = resultBlock, +) + +/** + * Calls the specified [Result]-returning function [resultBlock] + * encapsulating its progress and return value into an [Async] while + * posting its updates to the MutableState [state]. + * + * @param T the type of data returned by the operation. + * @param state the [MutableState] to post updates to. + * @param errorTransform a function to transform the error before posting it. + * @param resultBlock a suspending function that returns a [Result]. + * @return the [Result] returned by [resultBlock]. + */ +@OptIn(ExperimentalContracts::class) +@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE") +suspend inline fun runUpdatingState( + state: MutableState>, + errorTransform: (Throwable) -> Throwable = { it }, + resultBlock: suspend () -> Result, +): Result { + contract { + callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE) } - this().fold( + val prevData = state.value.dataOrNull() + state.value = Async.Loading(prevData = prevData) + return resultBlock().fold( onSuccess = { state.value = Async.Success(it) + Result.success(it) }, onFailure = { - state.value = Async.Failure(it) + val error = errorTransform(it) + state.value = Async.Failure( + error = error, + prevData = prevData, + ) + Result.failure(error) } ) } - -fun Async.isLoading(): Boolean { - return this is Async.Loading -} diff --git a/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncKtTest.kt b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncKtTest.kt new file mode 100644 index 0000000000..4b6c75a108 --- /dev/null +++ b/libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncKtTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.architecture + +import androidx.compose.runtime.MutableState +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AsyncKtTest { + @Test + fun `updates state when block returns success`() = runTest { + val state = TestableMutableState>(Async.Uninitialized) + + val result = runUpdatingState(state) { + delay(1) + Result.success(1) + } + + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(1) + + assertThat(state.popFirst()).isEqualTo(Async.Uninitialized) + assertThat(state.popFirst()).isEqualTo(Async.Loading(null)) + assertThat(state.popFirst()).isEqualTo(Async.Success(1)) + state.assertNoMoreValues() + } + + @Test + fun `updates state when block returns failure`() = runTest { + val state = TestableMutableState>(Async.Uninitialized) + + val result = runUpdatingState(state) { + delay(1) + Result.failure(MyThrowable("hello")) + } + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello")) + + assertThat(state.popFirst()).isEqualTo(Async.Uninitialized) + assertThat(state.popFirst()).isEqualTo(Async.Loading(null)) + assertThat(state.popFirst()).isEqualTo(Async.Failure(MyThrowable("hello"))) + state.assertNoMoreValues() + } + + @Test + fun `updates state when block returns failure transforming the error`() = runTest { + val state = TestableMutableState>(Async.Uninitialized) + + val result = runUpdatingState(state, { MyThrowable(it.message + " world") }) { + delay(1) + Result.failure(MyThrowable("hello")) + } + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello world")) + + assertThat(state.popFirst()).isEqualTo(Async.Uninitialized) + assertThat(state.popFirst()).isEqualTo(Async.Loading(null)) + assertThat(state.popFirst()).isEqualTo(Async.Failure(MyThrowable("hello world"))) + state.assertNoMoreValues() + } +} + +/** + * A fake [MutableState] that allows to record all the states that were set. + */ +private class TestableMutableState( + value: T +) : MutableState { + + private val _deque = ArrayDeque(listOf(value)) + + override var value: T + get() = _deque.last() + set(value) { + _deque.addLast(value) + } + + /** + * Returns the states that were set in the order they were set. + */ + fun popFirst(): T = _deque.removeFirst() + + fun assertNoMoreValues() { + assertThat(_deque).isEmpty() + } + + override operator fun component1(): T = value + + override operator fun component2(): (T) -> Unit = { value = it } +} + +/** + * An exception that is also a data class so we can compare it using equals. + */ +private data class MyThrowable(val myMessage: String) : Throwable(myMessage)