Merge pull request #2166 from element-hq/feature/bma/fixFlakyTest

Introduce AsyncAction with a Confirmation state and use it for logout action
This commit is contained in:
Benoit Marty 2024-01-05 10:30:43 +01:00 committed by GitHub
commit 24a3c2baf7
173 changed files with 1154 additions and 1022 deletions

View file

@ -34,7 +34,7 @@ import io.element.android.features.call.data.WidgetMessage
import io.element.android.features.call.utils.CallWidgetProvider import io.element.android.features.call.utils.CallWidgetProvider
import io.element.android.features.call.utils.WidgetMessageInterceptor import io.element.android.features.call.utils.WidgetMessageInterceptor
import io.element.android.features.call.utils.WidgetMessageSerializer import io.element.android.features.call.utils.WidgetMessageSerializer
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -75,7 +75,7 @@ class CallScreenPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): CallScreenState { override fun present(): CallScreenState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val urlState = remember { mutableStateOf<Async<String>>(Async.Uninitialized) } val urlState = remember { mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized) }
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) } val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) } val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) } var isJoinedCall by rememberSaveable { mutableStateOf(false) }
@ -154,7 +154,7 @@ class CallScreenPresenter @AssistedInject constructor(
private fun CoroutineScope.loadUrl( private fun CoroutineScope.loadUrl(
inputs: CallType, inputs: CallType,
urlState: MutableState<Async<String>>, urlState: MutableState<AsyncData<String>>,
callWidgetDriver: MutableState<MatrixWidgetDriver?>, callWidgetDriver: MutableState<MatrixWidgetDriver?>,
) = launch { ) = launch {
urlState.runCatchingUpdatingState { urlState.runCatchingUpdatingState {

View file

@ -16,10 +16,10 @@
package io.element.android.features.call.ui package io.element.android.features.call.ui
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
data class CallScreenState( data class CallScreenState(
val urlState: Async<String>, val urlState: AsyncData<String>,
val userAgent: String, val userAgent: String,
val isInWidgetMode: Boolean, val isInWidgetMode: Boolean,
val eventSink: (CallScreenEvents) -> Unit, val eventSink: (CallScreenEvents) -> Unit,

View file

@ -36,7 +36,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.call.R import io.element.android.features.call.R
import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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
@ -96,7 +96,7 @@ internal fun CallScreenView(
@Composable @Composable
private fun CallWebView( private fun CallWebView(
url: Async<String>, url: AsyncData<String>,
userAgent: String, userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit, onPermissionsRequested: (PermissionRequest) -> Unit,
onWebViewCreated: (WebView) -> Unit, onWebViewCreated: (WebView) -> Unit,
@ -116,7 +116,7 @@ private fun CallWebView(
} }
}, },
update = { webView -> update = { webView ->
if (url is Async.Success && webView.url != url.data) { if (url is AsyncData.Success && webView.url != url.data) {
webView.loadUrl(url.data) webView.loadUrl(url.data)
} }
}, },
@ -161,7 +161,7 @@ internal fun CallScreenViewPreview() {
ElementPreview { ElementPreview {
CallScreenView( CallScreenView(
state = CallScreenState( state = CallScreenState(
urlState = Async.Success("https://call.element.io/some-actual-call?with=parameters"), urlState = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
isInWidgetMode = false, isInWidgetMode = false,
userAgent = "", userAgent = "",
eventSink = {}, eventSink = {},

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.CallType import io.element.android.features.call.CallType
import io.element.android.features.call.utils.FakeCallWidgetProvider import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -62,7 +62,7 @@ class CallScreenPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isEqualTo(Async.Success("https://call.element.io")) assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.isInWidgetMode).isFalse() assertThat(initialState.isInWidgetMode).isFalse()
} }
} }
@ -83,7 +83,7 @@ class CallScreenPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java) assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.isInWidgetMode).isTrue() assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1) assertThat(widgetDriver.runCalledCount).isEqualTo(1)

View file

@ -17,7 +17,7 @@
package io.element.android.features.createroom.api package io.element.android.features.createroom.api
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
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
@ -27,5 +27,5 @@ interface StartDMAction {
* @param userId The user to start a DM with. * @param userId The user to start a DM with.
* @param actionState The state to update with the result of the action. * @param actionState The state to update with the result of the action.
*/ */
suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>)
} }

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.MutableState
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.api.StartDMAction
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
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
@ -36,17 +36,17 @@ class DefaultStartDMAction @Inject constructor(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : StartDMAction { ) : StartDMAction {
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) { override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
actionState.value = Async.Loading() actionState.value = AsyncAction.Loading
when (val result = matrixClient.startDM(userId)) { when (val result = matrixClient.startDM(userId)) {
is StartDMResult.Success -> { is StartDMResult.Success -> {
if (result.isNew) { if (result.isNew) {
analyticsService.capture(CreatedRoom(isDM = true)) analyticsService.capture(CreatedRoom(isDM = true))
} }
actionState.value = Async.Success(result.roomId) actionState.value = AsyncAction.Success(result.roomId)
} }
is StartDMResult.Failure -> { is StartDMResult.Failure -> {
actionState.value = Async.Failure(result.throwable) actionState.value = AsyncAction.Failure(result.throwable)
} }
} }
} }

View file

@ -29,7 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
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.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.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
@ -91,10 +91,10 @@ class ConfigureRoomPresenter @Inject constructor(
} }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) } val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
fun createRoom(config: CreateRoomConfig) { fun createRoom(config: CreateRoomConfig) {
createRoomAction.value = Async.Uninitialized createRoomAction.value = AsyncAction.Uninitialized
localCoroutineScope.createRoom(config, createRoomAction) localCoroutineScope.createRoom(config, createRoomAction)
} }
@ -118,7 +118,7 @@ class ConfigureRoomPresenter @Inject constructor(
} }
} }
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = Async.Uninitialized ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
} }
} }
@ -133,7 +133,7 @@ class ConfigureRoomPresenter @Inject constructor(
private fun CoroutineScope.createRoom( private fun CoroutineScope.createRoom(
config: CreateRoomConfig, config: CreateRoomConfig,
createRoomAction: MutableState<Async<RoomId>> createRoomAction: MutableState<AsyncAction<RoomId>>
) = launch { ) = launch {
suspend { suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) } val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }

View file

@ -16,17 +16,17 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import io.element.android.libraries.matrix.ui.media.AvatarAction
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.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class ConfigureRoomState( data class ConfigureRoomState(
val config: CreateRoomConfig, val config: CreateRoomConfig,
val avatarActions: ImmutableList<AvatarAction>, val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: Async<RoomId>, val createRoomAction: AsyncAction<RoomId>,
val cameraPermissionState: PermissionsState, val cameraPermissionState: PermissionsState,
val eventSink: (ConfigureRoomEvents) -> Unit val eventSink: (ConfigureRoomEvents) -> Unit
) { ) {

View file

@ -19,7 +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.createroom.impl.userlist.aListOfSelectedUsers import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.api.aPermissionsState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -41,7 +41,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
fun aConfigureRoomState() = ConfigureRoomState( fun aConfigureRoomState() = ConfigureRoomState(
config = CreateRoomConfig(), config = CreateRoomConfig(),
avatarActions = persistentListOf(), avatarActions = persistentListOf(),
createRoomAction = Async.Uninitialized, createRoomAction = AsyncAction.Uninitialized,
cameraPermissionState = aPermissionsState(showDialog = false), cameraPermissionState = aPermissionsState(showDialog = false),
eventSink = { }, eventSink = { },
) )

View file

@ -49,7 +49,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.RoomPrivacyOption import io.element.android.features.createroom.impl.components.RoomPrivacyOption
import io.element.android.libraries.designsystem.components.LabelledTextField import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.async.AsyncView 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
@ -148,9 +149,13 @@ fun ConfigureRoomView(
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) } onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
) )
AsyncView( AsyncActionView(
async = state.createRoomAction, async = state.createRoomAction,
progressText = stringResource(CommonStrings.common_creating_room), progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_creating_room),
)
},
onSuccess = { onRoomCreated(it) }, onSuccess = { onRoomCreated(it) },
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) }, errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) }, onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },

View file

@ -26,7 +26,7 @@ import io.element.android.features.createroom.impl.userlist.SelectionMode
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.features.createroom.impl.userlist.UserListPresenter import io.element.android.features.createroom.impl.userlist.UserListPresenter
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
import io.element.android.libraries.architecture.Async 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.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -55,14 +55,14 @@ class CreateRoomRootPresenter @Inject constructor(
val userListState = presenter.present() val userListState = presenter.present()
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val startDmActionState: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) } val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvents(event: CreateRoomRootEvents) { fun handleEvents(event: CreateRoomRootEvents) {
when (event) { when (event) {
is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch { is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
startDMAction.execute(event.matrixUser.userId, startDmActionState) startDMAction.execute(event.matrixUser.userId, startDmActionState)
} }
CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = Async.Uninitialized CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
} }
} }

View file

@ -17,12 +17,12 @@
package io.element.android.features.createroom.impl.root package io.element.android.features.createroom.impl.root
import io.element.android.features.createroom.impl.userlist.UserListState import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
data class CreateRoomRootState( data class CreateRoomRootState(
val applicationName: String, val applicationName: String,
val userListState: UserListState, val userListState: UserListState,
val startDmAction: Async<RoomId>, val startDmAction: AsyncAction<RoomId>,
val eventSink: (CreateRoomRootEvents) -> Unit, val eventSink: (CreateRoomRootEvents) -> Unit,
) )

View file

@ -18,8 +18,8 @@ package io.element.android.features.createroom.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.features.createroom.impl.userlist.aUserListState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.api.UserSearchResult
@ -30,7 +30,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
get() = sequenceOf( get() = sequenceOf(
aCreateRoomRootState(), aCreateRoomRootState(),
aCreateRoomRootState().copy( aCreateRoomRootState().copy(
startDmAction = Async.Loading(), startDmAction = AsyncAction.Loading,
userListState = aMatrixUser().let { userListState = aMatrixUser().let {
aUserListState().copy( aUserListState().copy(
searchQuery = it.userId.value, searchQuery = it.userId.value,
@ -41,7 +41,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
} }
), ),
aCreateRoomRootState().copy( aCreateRoomRootState().copy(
startDmAction = Async.Failure(Throwable("error")), startDmAction = AsyncAction.Failure(Throwable("error")),
userListState = aMatrixUser().let { userListState = aMatrixUser().let {
aUserListState().copy( aUserListState().copy(
searchQuery = it.userId.value, searchQuery = it.userId.value,
@ -57,6 +57,6 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
fun aCreateRoomRootState() = CreateRoomRootState( fun aCreateRoomRootState() = CreateRoomRootState(
eventSink = {}, eventSink = {},
applicationName = "Element X Preview", applicationName = "Element X Preview",
startDmAction = Async.Uninitialized, startDmAction = AsyncAction.Uninitialized,
userListState = aUserListState(), userListState = aUserListState(),
) )

View file

@ -38,7 +38,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.UserListView import io.element.android.features.createroom.impl.components.UserListView
import io.element.android.libraries.designsystem.components.async.AsyncView 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.icons.CompoundDrawables import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -93,9 +94,13 @@ fun CreateRoomRootView(
} }
} }
AsyncView( AsyncActionView(
async = state.startDmAction, async = state.startDmAction,
progressText = stringResource(CommonStrings.common_starting_chat), progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_starting_chat),
)
},
onSuccess = { onOpenDM(it) }, onSuccess = { onOpenDM(it) },
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) }, errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
onRetry = { onRetry = {

View file

@ -19,7 +19,7 @@ package io.element.android.features.createroom.impl
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
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.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -39,9 +39,9 @@ class DefaultStartDMActionTests {
givenFindDmResult(A_ROOM_ID) givenFindDmResult(A_ROOM_ID)
} }
val action = createStartDMAction(matrixClient) val action = createStartDMAction(matrixClient)
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized) val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
action.execute(A_USER_ID, state) action.execute(A_USER_ID, state)
assertThat(state.value).isEqualTo(Async.Success(A_ROOM_ID)) assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID))
} }
@Test @Test
@ -52,9 +52,9 @@ class DefaultStartDMActionTests {
} }
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val action = createStartDMAction(matrixClient, analyticsService) val action = createStartDMAction(matrixClient, analyticsService)
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized) val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
action.execute(A_USER_ID, state) action.execute(A_USER_ID, state)
assertThat(state.value).isEqualTo(Async.Success(A_ROOM_ID)) assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID))
assertThat(analyticsService.capturedEvents).containsExactly(CreatedRoom(isDM = true)) assertThat(analyticsService.capturedEvents).containsExactly(CreatedRoom(isDM = true))
} }
@ -65,9 +65,9 @@ class DefaultStartDMActionTests {
givenCreateDmResult(Result.failure(A_THROWABLE)) givenCreateDmResult(Result.failure(A_THROWABLE))
} }
val action = createStartDMAction(matrixClient) val action = createStartDMAction(matrixClient)
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized) val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
action.execute(A_USER_ID, state) action.execute(A_USER_ID, state)
assertThat(state.value).isEqualTo(Async.Failure<RoomId>(A_THROWABLE)) assertThat(state.value).isEqualTo(AsyncAction.Failure(A_THROWABLE))
} }
private fun createStartDMAction( private fun createStartDMAction(

View file

@ -25,7 +25,7 @@ import im.vector.app.features.analytics.plan.CreatedRoom
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.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId 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
@ -234,9 +234,9 @@ class ConfigureRoomPresenterTests {
fakeMatrixClient.givenCreateRoomResult(createRoomResult) fakeMatrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem() val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Success::class.java) assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull()) assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
} }
} }
@ -272,16 +272,16 @@ class ConfigureRoomPresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem() val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java) assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty() assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL)) fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Success::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
} }
} }
@ -297,22 +297,22 @@ class ConfigureRoomPresenterTests {
// Create // Create
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem() val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java) assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry // Retry
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterRetry = awaitItem() val stateAfterRetry = awaitItem()
assertThat(stateAfterRetry.createRoomAction).isInstanceOf(Async.Failure::class.java) assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterRetry.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull()) assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Cancel // Cancel
stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom) stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
} }

View file

@ -25,8 +25,7 @@ import io.element.android.features.createroom.impl.userlist.FakeUserListPresente
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
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.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -52,20 +51,20 @@ class CreateRoomRootPresenterTests {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.startDmAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName) assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName)
assertThat(initialState.userListState.selectedUsers).isEmpty() assertThat(initialState.userListState.selectedUsers).isEmpty()
assertThat(initialState.userListState.isSearchActive).isFalse() assertThat(initialState.userListState.isSearchActive).isFalse()
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
val matrixUser = MatrixUser(UserId("@name:domain")) val matrixUser = MatrixUser(UserId("@name:domain"))
val startDMSuccessResult = Async.Success(A_ROOM_ID) val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
val startDMFailureResult = Async.Failure<RoomId>(A_THROWABLE) val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
// Failure // Failure
startDMAction.givenExecuteResult(startDMFailureResult) startDMAction.givenExecuteResult(startDMFailureResult)
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(startDMFailureResult) assertThat(state.startDmAction).isEqualTo(startDMFailureResult)
state.eventSink(CreateRoomRootEvents.CancelStartDM) state.eventSink(CreateRoomRootEvents.CancelStartDM)
@ -74,10 +73,10 @@ class CreateRoomRootPresenterTests {
// Success // Success
startDMAction.givenExecuteResult(startDMSuccessResult) startDMAction.givenExecuteResult(startDMSuccessResult)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(Async.Uninitialized) assertThat(state.startDmAction).isEqualTo(AsyncAction.Uninitialized)
state.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) state.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
} }
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(startDMSuccessResult) assertThat(state.startDmAction).isEqualTo(startDMSuccessResult)
} }

View file

@ -18,7 +18,7 @@ package io.element.android.features.createroom.test
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.api.StartDMAction
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
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.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -26,14 +26,14 @@ import kotlinx.coroutines.delay
class FakeStartDMAction : StartDMAction { class FakeStartDMAction : StartDMAction {
private var executeResult: Async<RoomId> = Async.Success(A_ROOM_ID) private var executeResult: AsyncAction<RoomId> = AsyncAction.Success(A_ROOM_ID)
fun givenExecuteResult(result: Async<RoomId>) { fun givenExecuteResult(result: AsyncAction<RoomId>) {
executeResult = result executeResult = result
} }
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) { override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
actionState.value = Async.Loading() actionState.value = AsyncAction.Loading
delay(1) delay(1)
actionState.value = executeResult actionState.value = executeResult
} }

View file

@ -29,7 +29,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invitelist.api.SeenInvitesStore import io.element.android.features.invitelist.api.SeenInvitesStore
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -77,14 +77,14 @@ class InviteListPresenter @Inject constructor(
} }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) } val acceptedAction: MutableState<AsyncData<RoomId>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } val declinedAction: MutableState<AsyncData<Unit>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val decliningInvite: MutableState<InviteListInviteSummary?> = remember { mutableStateOf(null) } val decliningInvite: MutableState<InviteListInviteSummary?> = remember { mutableStateOf(null) }
fun handleEvent(event: InviteListEvents) { fun handleEvent(event: InviteListEvents) {
when (event) { when (event) {
is InviteListEvents.AcceptInvite -> { is InviteListEvents.AcceptInvite -> {
acceptedAction.value = Async.Uninitialized acceptedAction.value = AsyncData.Uninitialized
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
} }
@ -93,7 +93,7 @@ class InviteListPresenter @Inject constructor(
} }
is InviteListEvents.ConfirmDeclineInvite -> { is InviteListEvents.ConfirmDeclineInvite -> {
declinedAction.value = Async.Uninitialized declinedAction.value = AsyncData.Uninitialized
decliningInvite.value?.let { decliningInvite.value?.let {
localCoroutineScope.declineInvite(it.roomId, declinedAction) localCoroutineScope.declineInvite(it.roomId, declinedAction)
} }
@ -105,11 +105,11 @@ class InviteListPresenter @Inject constructor(
} }
is InviteListEvents.DismissAcceptError -> { is InviteListEvents.DismissAcceptError -> {
acceptedAction.value = Async.Uninitialized acceptedAction.value = AsyncData.Uninitialized
} }
is InviteListEvents.DismissDeclineError -> { is InviteListEvents.DismissDeclineError -> {
declinedAction.value = Async.Uninitialized declinedAction.value = AsyncData.Uninitialized
} }
} }
} }
@ -137,7 +137,7 @@ class InviteListPresenter @Inject constructor(
) )
} }
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch { private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncData<RoomId>>) = launch {
suspend { suspend {
client.getRoom(roomId)?.use { client.getRoom(roomId)?.use {
it.join().getOrThrow() it.join().getOrThrow()
@ -148,7 +148,7 @@ class InviteListPresenter @Inject constructor(
}.runCatchingUpdatingState(acceptedAction) }.runCatchingUpdatingState(acceptedAction)
} }
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncData<Unit>>) = launch {
suspend { suspend {
client.getRoom(roomId)?.use { client.getRoom(roomId)?.use {
it.leave().getOrThrow() it.leave().getOrThrow()

View file

@ -18,7 +18,7 @@ package io.element.android.features.invitelist.impl
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -26,8 +26,8 @@ import kotlinx.collections.immutable.ImmutableList
data class InviteListState( data class InviteListState(
val inviteList: ImmutableList<InviteListInviteSummary>, val inviteList: ImmutableList<InviteListInviteSummary>,
val declineConfirmationDialog: InviteDeclineConfirmationDialog, val declineConfirmationDialog: InviteDeclineConfirmationDialog,
val acceptedAction: Async<RoomId>, val acceptedAction: AsyncData<RoomId>,
val declinedAction: Async<Unit>, val declinedAction: AsyncData<Unit>,
val eventSink: (InviteListEvents) -> Unit val eventSink: (InviteListEvents) -> Unit
) )

View file

@ -19,7 +19,7 @@ package io.element.android.features.invitelist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -32,16 +32,16 @@ open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
aInviteListState().copy(inviteList = persistentListOf()), aInviteListState().copy(inviteList = persistentListOf()),
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")), aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")),
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")), aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")),
aInviteListState().copy(acceptedAction = Async.Failure(Throwable("Whoops"))), aInviteListState().copy(acceptedAction = AsyncData.Failure(Throwable("Whoops"))),
aInviteListState().copy(declinedAction = Async.Failure(Throwable("Whoops"))), aInviteListState().copy(declinedAction = AsyncData.Failure(Throwable("Whoops"))),
) )
} }
internal fun aInviteListState() = InviteListState( internal fun aInviteListState() = InviteListState(
inviteList = aInviteListInviteSummaryList(), inviteList = aInviteListInviteSummaryList(),
declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden, declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
acceptedAction = Async.Uninitialized, acceptedAction = AsyncData.Uninitialized,
declinedAction = Async.Uninitialized, declinedAction = AsyncData.Uninitialized,
eventSink = {}, eventSink = {},
) )

View file

@ -35,7 +35,7 @@ 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.features.invitelist.impl.components.InviteSummaryRow import io.element.android.features.invitelist.impl.components.InviteSummaryRow
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@ -56,7 +56,7 @@ fun InviteListView(
onInviteAccepted: (RoomId) -> Unit, onInviteAccepted: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.acceptedAction is Async.Success) { if (state.acceptedAction is AsyncData.Success) {
LaunchedEffect(state.acceptedAction) { LaunchedEffect(state.acceptedAction) {
onInviteAccepted(state.acceptedAction.data) onInviteAccepted(state.acceptedAction.data)
} }
@ -89,7 +89,7 @@ fun InviteListView(
) )
} }
if (state.acceptedAction is Async.Failure) { if (state.acceptedAction is AsyncData.Failure) {
ErrorDialog( ErrorDialog(
content = stringResource(CommonStrings.error_unknown), content = stringResource(CommonStrings.error_unknown),
title = stringResource(CommonStrings.common_error), title = stringResource(CommonStrings.common_error),
@ -98,7 +98,7 @@ fun InviteListView(
) )
} }
if (state.declinedAction is Async.Failure) { if (state.declinedAction is AsyncData.Failure) {
ErrorDialog( ErrorDialog(
content = stringResource(CommonStrings.error_unknown), content = stringResource(CommonStrings.error_unknown),
title = stringResource(CommonStrings.common_error), title = stringResource(CommonStrings.common_error),

View file

@ -22,7 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.invitelist.api.SeenInvitesStore import io.element.android.features.invitelist.api.SeenInvitesStore
import io.element.android.features.invitelist.test.FakeSeenInvitesStore import io.element.android.features.invitelist.test.FakeSeenInvitesStore
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -245,7 +245,7 @@ class InviteListPresenterTests {
val newState = awaitItem() val newState = awaitItem()
assertThat(newState.declinedAction).isEqualTo(Async.Failure<Unit>(ex)) assertThat(newState.declinedAction).isEqualTo(AsyncData.Failure<Unit>(ex))
} }
} }
@ -277,7 +277,7 @@ class InviteListPresenterTests {
val newState = awaitItem() val newState = awaitItem()
assertThat(newState.declinedAction).isEqualTo(Async.Uninitialized) assertThat(newState.declinedAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -300,7 +300,7 @@ class InviteListPresenterTests {
val newState = awaitItem() val newState = awaitItem()
assertThat(newState.acceptedAction).isEqualTo(Async.Success(A_ROOM_ID)) assertThat(newState.acceptedAction).isEqualTo(AsyncData.Success(A_ROOM_ID))
assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1) assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
} }
} }
@ -323,7 +323,7 @@ class InviteListPresenterTests {
val originalState = awaitItem() val originalState = awaitItem()
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0])) originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
assertThat(awaitItem().acceptedAction).isEqualTo(Async.Failure<RoomId>(ex)) assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure<RoomId>(ex))
} }
} }
@ -350,7 +350,7 @@ class InviteListPresenterTests {
originalState.eventSink(InviteListEvents.DismissAcceptError) originalState.eventSink(InviteListEvents.DismissAcceptError)
val newState = awaitItem() val newState = awaitItem()
assertThat(newState.acceptedAction).isEqualTo(Async.Uninitialized) assertThat(newState.acceptedAction).isEqualTo(AsyncData.Uninitialized)
} }
} }

View file

@ -29,7 +29,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
@ -49,11 +49,11 @@ class PinUnlockPresenter @Inject constructor(
@Composable @Composable
override fun present(): PinUnlockState { override fun present(): PinUnlockState {
val pinEntryState = remember { val pinEntryState = remember {
mutableStateOf<Async<PinEntry>>(Async.Uninitialized) mutableStateOf<AsyncData<PinEntry>>(AsyncData.Uninitialized)
} }
val pinEntry by pinEntryState val pinEntry by pinEntryState
var remainingAttempts by remember { var remainingAttempts by remember {
mutableStateOf<Async<Int>>(Async.Uninitialized) mutableStateOf<AsyncData<Int>>(AsyncData.Uninitialized)
} }
var showWrongPinTitle by rememberSaveable { var showWrongPinTitle by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
@ -62,7 +62,7 @@ class PinUnlockPresenter @Inject constructor(
mutableStateOf(false) mutableStateOf(false)
} }
val signOutAction = remember { val signOutAction = remember {
mutableStateOf<Async<String?>>(Async.Uninitialized) mutableStateOf<AsyncData<String?>>(AsyncData.Uninitialized)
} }
var biometricUnlockResult by remember { var biometricUnlockResult by remember {
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null) mutableStateOf<BiometricUnlock.AuthenticationResult?>(null)
@ -91,7 +91,7 @@ class PinUnlockPresenter @Inject constructor(
} }
} }
val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber() val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber()
remainingAttempts = Async.Success(remainingAttemptsNumber) remainingAttempts = AsyncData.Success(remainingAttemptsNumber)
if (remainingAttemptsNumber == 0) { if (remainingAttemptsNumber == 0) {
showSignOutPrompt = true showSignOutPrompt = true
} }
@ -139,46 +139,46 @@ class PinUnlockPresenter @Inject constructor(
) )
} }
private fun Async<PinEntry>.isComplete(): Boolean { private fun AsyncData<PinEntry>.isComplete(): Boolean {
return dataOrNull()?.isComplete().orFalse() return dataOrNull()?.isComplete().orFalse()
} }
private fun Async<PinEntry>.toText(): String { private fun AsyncData<PinEntry>.toText(): String {
return dataOrNull()?.toText() ?: "" return dataOrNull()?.toText() ?: ""
} }
private fun Async<PinEntry>.clear(): Async<PinEntry> { private fun AsyncData<PinEntry>.clear(): AsyncData<PinEntry> {
return when (this) { return when (this) {
is Async.Success -> Async.Success(data.clear()) is AsyncData.Success -> AsyncData.Success(data.clear())
else -> this else -> this
} }
} }
private fun Async<PinEntry>.process(pinKeypadModel: PinKeypadModel): Async<PinEntry> { private fun AsyncData<PinEntry>.process(pinKeypadModel: PinKeypadModel): AsyncData<PinEntry> {
return when (this) { return when (this) {
is Async.Success -> { is AsyncData.Success -> {
val pinEntry = when (pinKeypadModel) { val pinEntry = when (pinKeypadModel) {
PinKeypadModel.Back -> data.deleteLast() PinKeypadModel.Back -> data.deleteLast()
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number) is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
PinKeypadModel.Empty -> data PinKeypadModel.Empty -> data
} }
Async.Success(pinEntry) AsyncData.Success(pinEntry)
} }
else -> this else -> this
} }
} }
private fun Async<PinEntry>.process(pinEntryAsText: String): Async<PinEntry> { private fun AsyncData<PinEntry>.process(pinEntryAsText: String): AsyncData<PinEntry> {
return when (this) { return when (this) {
is Async.Success -> { is AsyncData.Success -> {
val pinEntry = data.fillWith(pinEntryAsText) val pinEntry = data.fillWith(pinEntryAsText)
Async.Success(pinEntry) AsyncData.Success(pinEntry)
} }
else -> this else -> this
} }
} }
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch { private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
suspend { suspend {
matrixClient.logout(ignoreSdkError = true) matrixClient.logout(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction) }.runCatchingUpdatingState(signOutAction)

View file

@ -19,21 +19,21 @@ package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
data class PinUnlockState( data class PinUnlockState(
val pinEntry: Async<PinEntry>, val pinEntry: AsyncData<PinEntry>,
val showWrongPinTitle: Boolean, val showWrongPinTitle: Boolean,
val remainingAttempts: Async<Int>, val remainingAttempts: AsyncData<Int>,
val showSignOutPrompt: Boolean, val showSignOutPrompt: Boolean,
val signOutAction: Async<String?>, val signOutAction: AsyncData<String?>,
val showBiometricUnlock: Boolean, val showBiometricUnlock: Boolean,
val isUnlocked: Boolean, val isUnlocked: Boolean,
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?, val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
val eventSink: (PinUnlockEvents) -> Unit val eventSink: (PinUnlockEvents) -> Unit
) { ) {
val isSignOutPromptCancellable = when (remainingAttempts) { val isSignOutPromptCancellable = when (remainingAttempts) {
is Async.Success -> remainingAttempts.data > 0 is AsyncData.Success -> remainingAttempts.data > 0
else -> true else -> true
} }

View file

@ -19,7 +19,7 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> { open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
override val values: Sequence<PinUnlockState> override val values: Sequence<PinUnlockState>
@ -30,7 +30,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showSignOutPrompt = true),
aPinUnlockState(showBiometricUnlock = false), aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = Async.Loading()), aPinUnlockState(signOutAction = AsyncData.Loading()),
) )
} }
@ -42,11 +42,11 @@ fun aPinUnlockState(
showBiometricUnlock: Boolean = true, showBiometricUnlock: Boolean = true,
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null, biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
isUnlocked: Boolean = false, isUnlocked: Boolean = false,
signOutAction: Async<String?> = Async.Uninitialized, signOutAction: AsyncData<String?> = AsyncData.Uninitialized,
) = PinUnlockState( ) = PinUnlockState(
pinEntry = Async.Success(pinEntry), pinEntry = AsyncData.Success(pinEntry),
showWrongPinTitle = showWrongPinTitle, showWrongPinTitle = showWrongPinTitle,
remainingAttempts = Async.Success(remainingAttempts), remainingAttempts = AsyncData.Success(remainingAttempts),
showSignOutPrompt = showSignOutPrompt, showSignOutPrompt = showSignOutPrompt,
showBiometricUnlock = showBiometricUnlock, showBiometricUnlock = showBiometricUnlock,
signOutAction = signOutAction, signOutAction = signOutAction,

View file

@ -56,7 +56,7 @@ import io.element.android.features.lockscreen.impl.components.PinEntryTextField
import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinDigit
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@ -92,7 +92,7 @@ fun PinUnlockView(
onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) }, onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) },
) )
} }
if (state.signOutAction is Async.Loading) { if (state.signOutAction is AsyncData.Loading) {
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
} }
if (state.showBiometricUnlockError) { if (state.showBiometricUnlockError) {
@ -335,7 +335,7 @@ private fun PinUnlockHeader(
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
color = subtitleColor, color = subtitleColor,
) )
if (!isInAppUnlock && state.pinEntry is Async.Success) { if (!isInAppUnlock && state.pinEntry is AsyncData.Success) {
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
PinDotsRow(state.pinEntry.data) PinDotsRow(state.pinEntry.data)
} }

View file

@ -28,7 +28,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.pin.model.assertText
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
@ -48,15 +48,15 @@ class PinUnlockPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.pinEntry).isInstanceOf(Async.Uninitialized::class.java) assertThat(state.pinEntry).isInstanceOf(AsyncData.Uninitialized::class.java)
assertThat(state.showWrongPinTitle).isFalse() assertThat(state.showWrongPinTitle).isFalse()
assertThat(state.showSignOutPrompt).isFalse() assertThat(state.showSignOutPrompt).isFalse()
assertThat(state.isUnlocked).isFalse() assertThat(state.isUnlocked).isFalse()
assertThat(state.signOutAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(state.signOutAction).isInstanceOf(AsyncData.Uninitialized::class.java)
assertThat(state.remainingAttempts).isInstanceOf(Async.Uninitialized::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Uninitialized::class.java)
} }
consumeItemsUntilPredicate { consumeItemsUntilPredicate {
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
}.last().also { state -> }.last().also { state ->
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1')))
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2')))
@ -83,7 +83,7 @@ class PinUnlockPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = consumeItemsUntilPredicate { val initialState = consumeItemsUntilPredicate {
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
}.last() }.last()
val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0
repeat(numberOfAttempts) { repeat(numberOfAttempts) {
@ -107,7 +107,7 @@ class PinUnlockPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
consumeItemsUntilPredicate { consumeItemsUntilPredicate {
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
}.last().also { state -> }.last().also { state ->
state.eventSink(PinUnlockEvents.OnForgetPin) state.eventSink(PinUnlockEvents.OnForgetPin)
} }
@ -125,12 +125,12 @@ class PinUnlockPresenterTest {
state.eventSink(PinUnlockEvents.SignOut) state.eventSink(PinUnlockEvents.SignOut)
} }
consumeItemsUntilPredicate { state -> consumeItemsUntilPredicate { state ->
state.signOutAction is Async.Success state.signOutAction is AsyncData.Success
} }
} }
} }
private fun Async<PinEntry>.assertText(text: String) { private fun AsyncData<PinEntry>.assertText(text: String) {
dataOrNull()?.assertText(text) dataOrNull()?.assertText(text)
} }

View file

@ -24,7 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -41,14 +41,14 @@ class ChangeServerPresenter @Inject constructor(
override fun present(): ChangeServerState { override fun present(): ChangeServerState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val changeServerAction: MutableState<Async<Unit>> = remember { val changeServerAction: MutableState<AsyncData<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
fun handleEvents(event: ChangeServerEvents) { fun handleEvents(event: ChangeServerEvents) {
when (event) { when (event) {
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction) is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
} }
} }
@ -60,7 +60,7 @@ class ChangeServerPresenter @Inject constructor(
private fun CoroutineScope.changeServer( private fun CoroutineScope.changeServer(
data: AccountProvider, data: AccountProvider,
changeServerAction: MutableState<Async<Unit>>, changeServerAction: MutableState<AsyncData<Unit>>,
) = launch { ) = launch {
suspend { suspend {
authenticationService.setHomeserver(data.url).map { authenticationService.setHomeserver(data.url).map {

View file

@ -16,9 +16,9 @@
package io.element.android.features.login.impl.changeserver package io.element.android.features.login.impl.changeserver
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
data class ChangeServerState( data class ChangeServerState(
val changeServerAction: Async<Unit>, val changeServerAction: AsyncData<Unit>,
val eventSink: (ChangeServerEvents) -> Unit val eventSink: (ChangeServerEvents) -> Unit
) )

View file

@ -17,7 +17,7 @@
package io.element.android.features.login.impl.changeserver package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> { open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
override val values: Sequence<ChangeServerState> override val values: Sequence<ChangeServerState>
@ -27,6 +27,6 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
} }
fun aChangeServerState() = ChangeServerState( fun aChangeServerState() = ChangeServerState(
changeServerAction = Async.Uninitialized, changeServerAction = AsyncData.Uninitialized,
eventSink = {} eventSink = {}
) )

View file

@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -37,7 +37,7 @@ fun ChangeServerView(
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
when (state.changeServerAction) { when (state.changeServerAction) {
is Async.Failure -> { is AsyncData.Failure -> {
when (val error = state.changeServerAction.error) { when (val error = state.changeServerAction.error) {
is ChangeServerError.Error -> { is ChangeServerError.Error -> {
ErrorDialog( ErrorDialog(
@ -60,11 +60,11 @@ fun ChangeServerView(
} }
} }
} }
is Async.Loading -> ProgressDialog() is AsyncData.Loading -> ProgressDialog()
is Async.Success -> LaunchedEffect(state.changeServerAction) { is AsyncData.Success -> LaunchedEffect(state.changeServerAction) {
onDone() onDone()
} }
Async.Uninitialized -> Unit AsyncData.Uninitialized -> Unit
} }
} }

View file

@ -26,7 +26,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.architecture.Async 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.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
@ -44,33 +44,33 @@ class OidcPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): OidcState { override fun present(): OidcState {
var requestState: Async<Unit> by remember { var requestState: AsyncAction<Unit> by remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncAction.Uninitialized)
} }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
fun handleCancel() { fun handleCancel() {
requestState = Async.Loading() requestState = AsyncAction.Loading
localCoroutineScope.launch { localCoroutineScope.launch {
authenticationService.cancelOidcLogin() authenticationService.cancelOidcLogin()
.fold( .fold(
onSuccess = { onSuccess = {
// Then go back // Then go back
requestState = Async.Success(Unit) requestState = AsyncAction.Success(Unit)
}, },
onFailure = { onFailure = {
requestState = Async.Failure(it) requestState = AsyncAction.Failure(it)
} }
) )
} }
} }
fun handleSuccess(url: String) { fun handleSuccess(url: String) {
requestState = Async.Loading() requestState = AsyncAction.Loading
localCoroutineScope.launch { localCoroutineScope.launch {
authenticationService.loginWithOidc(url) authenticationService.loginWithOidc(url)
.onFailure { .onFailure {
requestState = Async.Failure(it) requestState = AsyncAction.Failure(it)
} }
// On success, the node tree will be updated, there is nothing to do // On success, the node tree will be updated, there is nothing to do
} }
@ -87,7 +87,7 @@ class OidcPresenter @AssistedInject constructor(
when (event) { when (event) {
OidcEvents.Cancel -> handleCancel() OidcEvents.Cancel -> handleCancel()
is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction) is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction)
OidcEvents.ClearError -> requestState = Async.Uninitialized OidcEvents.ClearError -> requestState = AsyncAction.Uninitialized
} }
} }

View file

@ -16,11 +16,11 @@
package io.element.android.features.login.impl.oidc.webview package io.element.android.features.login.impl.oidc.webview
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
data class OidcState( data class OidcState(
val oidcDetails: OidcDetails, val oidcDetails: OidcDetails,
val requestState: Async<Unit>, val requestState: AsyncAction<Unit>,
val eventSink: (OidcEvents) -> Unit val eventSink: (OidcEvents) -> Unit
) )

View file

@ -17,20 +17,20 @@
package io.element.android.features.login.impl.oidc.webview package io.element.android.features.login.impl.oidc.webview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
open class OidcStateProvider : PreviewParameterProvider<OidcState> { open class OidcStateProvider : PreviewParameterProvider<OidcState> {
override val values: Sequence<OidcState> override val values: Sequence<OidcState>
get() = sequenceOf( get() = sequenceOf(
aOidcState(), aOidcState(),
aOidcState().copy(requestState = Async.Loading()), aOidcState().copy(requestState = AsyncAction.Loading),
) )
} }
fun aOidcState() = OidcState( fun aOidcState() = OidcState(
oidcDetails = aOidcDetails(), oidcDetails = aOidcDetails(),
requestState = Async.Uninitialized, requestState = AsyncAction.Uninitialized,
eventSink = {} eventSink = {}
) )

View file

@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.features.login.impl.oidc.OidcUrlParser import io.element.android.features.login.impl.oidc.OidcUrlParser
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.async.AsyncActionView
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
@ -76,7 +76,7 @@ fun OidcView(
} }
) )
AsyncView( AsyncActionView(
async = state.requestState, async = state.requestState,
onSuccess = { onNavigateBack() }, onSuccess = { onNavigateBack() },
onErrorDismiss = { state.eventSink(OidcEvents.ClearError) } onErrorDismiss = { state.eventSink(OidcEvents.ClearError) }

View file

@ -32,7 +32,7 @@ import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -61,8 +61,8 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
val accountProvider by accountProviderDataSource.flow().collectAsState() val accountProvider by accountProviderDataSource.flow().collectAsState()
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val loginFlowAction: MutableState<Async<LoginFlow>> = remember { val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -78,7 +78,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
ConfirmAccountProviderEvents.Continue -> { ConfirmAccountProviderEvents.Continue -> {
localCoroutineScope.submit(accountProvider.url, loginFlowAction) localCoroutineScope.submit(accountProvider.url, loginFlowAction)
} }
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = AsyncData.Uninitialized
} }
} }
@ -92,7 +92,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
private fun CoroutineScope.submit( private fun CoroutineScope.submit(
homeserverUrl: String, homeserverUrl: String,
loginFlowAction: MutableState<Async<LoginFlow>>, loginFlowAction: MutableState<AsyncData<LoginFlow>>,
) = launch { ) = launch {
suspend { suspend {
authenticationService.setHomeserver(homeserverUrl).map { authenticationService.setHomeserver(homeserverUrl).map {
@ -111,17 +111,17 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
private suspend fun onOidcAction( private suspend fun onOidcAction(
oidcAction: OidcAction, oidcAction: OidcAction,
loginFlowAction: MutableState<Async<LoginFlow>>, loginFlowAction: MutableState<AsyncData<LoginFlow>>,
) { ) {
loginFlowAction.value = Async.Loading() loginFlowAction.value = AsyncData.Loading()
when (oidcAction) { when (oidcAction) {
OidcAction.GoBack -> { OidcAction.GoBack -> {
authenticationService.cancelOidcLogin() authenticationService.cancelOidcLogin()
.onSuccess { .onSuccess {
loginFlowAction.value = Async.Uninitialized loginFlowAction.value = AsyncData.Uninitialized
} }
.onFailure { failure -> .onFailure { failure ->
loginFlowAction.value = Async.Failure(failure) loginFlowAction.value = AsyncData.Failure(failure)
} }
} }
is OidcAction.Success -> { is OidcAction.Success -> {
@ -130,7 +130,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
defaultLoginUserStory.setLoginFlowIsDone(true) defaultLoginUserStory.setLoginFlowIsDone(true)
} }
.onFailure { failure -> .onFailure { failure ->
loginFlowAction.value = Async.Failure(failure) loginFlowAction.value = AsyncData.Failure(failure)
} }
} }
} }

View file

@ -17,17 +17,17 @@
package io.element.android.features.login.impl.screens.confirmaccountprovider package io.element.android.features.login.impl.screens.confirmaccountprovider
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.auth.OidcDetails
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ConfirmAccountProviderState( data class ConfirmAccountProviderState(
val accountProvider: AccountProvider, val accountProvider: AccountProvider,
val isAccountCreation: Boolean, val isAccountCreation: Boolean,
val loginFlow: Async<LoginFlow>, val loginFlow: AsyncData<LoginFlow>,
val eventSink: (ConfirmAccountProviderEvents) -> Unit val eventSink: (ConfirmAccountProviderEvents) -> Unit
) { ) {
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is AsyncData.Uninitialized || loginFlow is AsyncData.Loading)
} }
sealed interface LoginFlow { sealed interface LoginFlow {

View file

@ -18,7 +18,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<ConfirmAccountProviderState> { open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<ConfirmAccountProviderState> {
override val values: Sequence<ConfirmAccountProviderState> override val values: Sequence<ConfirmAccountProviderState>
@ -31,6 +31,6 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
fun aConfirmAccountProviderState() = ConfirmAccountProviderState( fun aConfirmAccountProviderState() = ConfirmAccountProviderState(
accountProvider = anAccountProvider(), accountProvider = anAccountProvider(),
isAccountCreation = false, isAccountCreation = false,
loginFlow = Async.Uninitialized, loginFlow = AsyncData.Uninitialized,
eventSink = {} eventSink = {}
) )

View file

@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@ -56,7 +56,7 @@ fun ConfirmAccountProviderView(
) { ) {
val isLoading by remember(state.loginFlow) { val isLoading by remember(state.loginFlow) {
derivedStateOf { derivedStateOf {
state.loginFlow is Async.Loading state.loginFlow is AsyncData.Loading
} }
} }
val eventSink = state.eventSink val eventSink = state.eventSink
@ -107,7 +107,7 @@ fun ConfirmAccountProviderView(
} }
) { ) {
when (state.loginFlow) { when (state.loginFlow) {
is Async.Failure -> { is AsyncData.Failure -> {
when (val error = state.loginFlow.error) { when (val error = state.loginFlow.error) {
is ChangeServerError.Error -> { is ChangeServerError.Error -> {
ErrorDialog( ErrorDialog(
@ -127,14 +127,14 @@ fun ConfirmAccountProviderView(
} }
} }
} }
is Async.Loading -> Unit // The Continue button shows the loading state is AsyncData.Loading -> Unit // The Continue button shows the loading state
is Async.Success -> { is AsyncData.Success -> {
when (val loginFlowState = state.loginFlow.data) { when (val loginFlowState = state.loginFlow.data) {
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails) is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
LoginFlow.PasswordLogin -> onLoginPasswordNeeded() LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
} }
} }
Async.Uninitialized -> Unit AsyncData.Uninitialized -> Unit
} }
} }
} }

View file

@ -26,7 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -43,8 +43,8 @@ class LoginPasswordPresenter @Inject constructor(
@Composable @Composable
override fun present(): LoginPasswordState { override fun present(): LoginPasswordState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val loginAction: MutableState<Async<SessionId>> = remember { val loginAction: MutableState<AsyncData<SessionId>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
val formState = rememberSaveable { val formState = rememberSaveable {
@ -63,7 +63,7 @@ class LoginPasswordPresenter @Inject constructor(
LoginPasswordEvents.Submit -> { LoginPasswordEvents.Submit -> {
localCoroutineScope.submit(formState.value, loginAction) localCoroutineScope.submit(formState.value, loginAction)
} }
LoginPasswordEvents.ClearError -> loginAction.value = Async.Uninitialized LoginPasswordEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
} }
} }
@ -75,16 +75,16 @@ class LoginPasswordPresenter @Inject constructor(
) )
} }
private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState<Async<SessionId>>) = launch { private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
loggedInState.value = Async.Loading() loggedInState.value = AsyncData.Loading()
authenticationService.login(formState.login.trim(), formState.password) authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId -> .onSuccess { sessionId ->
// We will not navigate to the WaitList screen, so the login user story is done // We will not navigate to the WaitList screen, so the login user story is done
defaultLoginUserStory.setLoginFlowIsDone(true) defaultLoginUserStory.setLoginFlowIsDone(true)
loggedInState.value = Async.Success(sessionId) loggedInState.value = AsyncData.Success(sessionId)
} }
.onFailure { failure -> .onFailure { failure ->
loggedInState.value = Async.Failure(failure) loggedInState.value = AsyncData.Failure(failure)
} }
} }

View file

@ -18,18 +18,18 @@ package io.element.android.features.login.impl.screens.loginpassword
import android.os.Parcelable import android.os.Parcelable
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
data class LoginPasswordState( data class LoginPasswordState(
val accountProvider: AccountProvider, val accountProvider: AccountProvider,
val formState: LoginFormState, val formState: LoginFormState,
val loginAction: Async<SessionId>, val loginAction: AsyncData<SessionId>,
val eventSink: (LoginPasswordEvents) -> Unit val eventSink: (LoginPasswordEvents) -> Unit
) { ) {
val submitEnabled: Boolean val submitEnabled: Boolean
get() = loginAction !is Async.Failure && get() = loginAction !is AsyncData.Failure &&
formState.login.isNotEmpty() && formState.login.isNotEmpty() &&
formState.password.isNotEmpty() formState.password.isNotEmpty()
} }

View file

@ -18,22 +18,22 @@ package io.element.android.features.login.impl.screens.loginpassword
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> { open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> {
override val values: Sequence<LoginPasswordState> override val values: Sequence<LoginPasswordState>
get() = sequenceOf( get() = sequenceOf(
aLoginPasswordState(), aLoginPasswordState(),
// Loading // Loading
aLoginPasswordState().copy(loginAction = Async.Loading()), aLoginPasswordState().copy(loginAction = AsyncData.Loading()),
// Error // Error
aLoginPasswordState().copy(loginAction = Async.Failure(Exception("An error occurred"))), aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))),
) )
} }
fun aLoginPasswordState() = LoginPasswordState( fun aLoginPasswordState() = LoginPasswordState(
accountProvider = anAccountProvider(), accountProvider = anAccountProvider(),
formState = LoginFormState.Default, formState = LoginFormState.Default,
loginAction = Async.Uninitialized, loginAction = AsyncData.Uninitialized,
eventSink = {} eventSink = {}
) )

View file

@ -54,7 +54,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.isWaitListError import io.element.android.features.login.impl.error.isWaitListError
import io.element.android.features.login.impl.error.loginError import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
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.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@ -84,7 +84,7 @@ fun LoginPasswordView(
) { ) {
val isLoading by remember(state.loginAction) { val isLoading by remember(state.loginAction) {
derivedStateOf { derivedStateOf {
state.loginAction is Async.Loading state.loginAction is AsyncData.Loading
} }
} }
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -148,7 +148,7 @@ fun LoginPasswordView(
) )
Spacer(modifier = Modifier.height(60.dp)) Spacer(modifier = Modifier.height(60.dp))
if (state.loginAction is Async.Failure) { if (state.loginAction is AsyncData.Failure) {
when { when {
state.loginAction.error.isWaitListError() -> { state.loginAction.error.isWaitListError() -> {
onWaitListError(state.formState) onWaitListError(state.formState)
@ -224,7 +224,7 @@ private fun LoginForm(
) )
var passwordVisible by remember { mutableStateOf(false) } var passwordVisible by remember { mutableStateOf(false) }
if (state.loginAction is Async.Loading) { if (state.loginAction is AsyncData.Loading) {
// Ensure password is hidden when user submits the form // Ensure password is hidden when user submits the form
passwordVisible = false passwordVisible = false
} }

View file

@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.features.login.impl.resolver.HomeserverData
import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.features.login.impl.resolver.HomeserverResolver
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -46,8 +46,8 @@ class SearchAccountProviderPresenter @Inject constructor(
} }
val changeServerState = changeServerPresenter.present() val changeServerState = changeServerPresenter.present()
val data: MutableState<Async<List<HomeserverData>>> = remember { val data: MutableState<AsyncData<List<HomeserverData>>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
LaunchedEffect(userInput) { LaunchedEffect(userInput) {
@ -70,16 +70,16 @@ class SearchAccountProviderPresenter @Inject constructor(
) )
} }
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<Async<List<HomeserverData>>>) = launch { private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<AsyncData<List<HomeserverData>>>) = launch {
data.value = Async.Uninitialized data.value = AsyncData.Uninitialized
// Debounce // Debounce
delay(300) delay(300)
data.value = Async.Loading() data.value = AsyncData.Loading()
homeserverResolver.resolve(userInput).collect { homeserverResolver.resolve(userInput).collect {
data.value = Async.Success(it) data.value = AsyncData.Success(it)
} }
if (data.value !is Async.Success) { if (data.value !is AsyncData.Success) {
data.value = Async.Uninitialized data.value = AsyncData.Uninitialized
} }
} }
} }

View file

@ -18,12 +18,12 @@ package io.element.android.features.login.impl.screens.searchaccountprovider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.features.login.impl.resolver.HomeserverData
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class SearchAccountProviderState( data class SearchAccountProviderState(
val userInput: String, val userInput: String,
val userInputResult: Async<List<HomeserverData>>, val userInputResult: AsyncData<List<HomeserverData>>,
val changeServerState: ChangeServerState, val changeServerState: ChangeServerState,
val eventSink: (SearchAccountProviderEvents) -> Unit val eventSink: (SearchAccountProviderEvents) -> Unit
) )

View file

@ -20,20 +20,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.appconfig.AuthenticationConfig import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.features.login.impl.resolver.HomeserverData
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> { open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> {
override val values: Sequence<SearchAccountProviderState> override val values: Sequence<SearchAccountProviderState>
get() = sequenceOf( get() = sequenceOf(
aSearchAccountProviderState(), aSearchAccountProviderState(),
aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())), aSearchAccountProviderState(userInputResult = AsyncData.Success(aHomeserverDataList())),
// Add other state here // Add other state here
) )
} }
fun aSearchAccountProviderState( fun aSearchAccountProviderState(
userInput: String = "", userInput: String = "",
userInputResult: Async<List<HomeserverData>> = Async.Uninitialized, userInputResult: AsyncData<List<HomeserverData>> = AsyncData.Uninitialized,
) = SearchAccountProviderState( ) = SearchAccountProviderState(
userInput = userInput, userInput = userInput,
userInputResult = userInputResult, userInputResult = userInputResult,

View file

@ -55,7 +55,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderVie
import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerView import io.element.android.features.login.impl.changeserver.ChangeServerView
import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.features.login.impl.resolver.HomeserverData
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
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.form.textFieldState import io.element.android.libraries.designsystem.components.form.textFieldState
@ -152,10 +152,10 @@ fun SearchAccountProviderView(
} }
when (state.userInputResult) { when (state.userInputResult) {
is Async.Failure -> { is AsyncData.Failure -> {
// Ignore errors (let the user type more chars) // Ignore errors (let the user type more chars)
} }
is Async.Loading -> { is AsyncData.Loading -> {
item { item {
Box( Box(
modifier = Modifier modifier = Modifier
@ -167,7 +167,7 @@ fun SearchAccountProviderView(
} }
} }
} }
is Async.Success -> { is AsyncData.Success -> {
items(state.userInputResult.data) { homeserverData -> items(state.userInputResult.data) { homeserverData ->
val item = homeserverData.toAccountProvider() val item = homeserverData.toAccountProvider()
AccountProviderView( AccountProviderView(
@ -178,7 +178,7 @@ fun SearchAccountProviderView(
) )
} }
} }
Async.Uninitialized -> Unit AsyncData.Uninitialized -> Unit
} }
item { item {
Spacer(Modifier.height(32.dp)) Spacer(Modifier.height(32.dp))

View file

@ -27,7 +27,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@ -55,8 +55,8 @@ class WaitListPresenter @AssistedInject constructor(
authenticationService.getHomeserverDetails().value?.url ?: "server" authenticationService.getHomeserverDetails().value?.url ?: "server"
} }
val loginAction: MutableState<Async<SessionId>> = remember { val loginAction: MutableState<AsyncData<SessionId>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
val attemptNumber = remember { mutableIntStateOf(0) } val attemptNumber = remember { mutableIntStateOf(0) }
@ -70,7 +70,7 @@ class WaitListPresenter @AssistedInject constructor(
coroutineScope.loginAttempt(formState, loginAction) coroutineScope.loginAttempt(formState, loginAction)
} }
} }
WaitListEvents.ClearError -> loginAction.value = Async.Uninitialized WaitListEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
WaitListEvents.Continue -> defaultLoginUserStory.setLoginFlowIsDone(true) WaitListEvents.Continue -> defaultLoginUserStory.setLoginFlowIsDone(true)
} }
} }
@ -83,15 +83,15 @@ class WaitListPresenter @AssistedInject constructor(
) )
} }
private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState<Async<SessionId>>) = launch { private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
Timber.w("Attempt to login...") Timber.w("Attempt to login...")
loggedInState.value = Async.Loading() loggedInState.value = AsyncData.Loading()
authenticationService.login(formState.login.trim(), formState.password) authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId -> .onSuccess { sessionId ->
loggedInState.value = Async.Success(sessionId) loggedInState.value = AsyncData.Success(sessionId)
} }
.onFailure { failure -> .onFailure { failure ->
loggedInState.value = Async.Failure(failure) loggedInState.value = AsyncData.Failure(failure)
} }
} }
} }

View file

@ -16,13 +16,13 @@
package io.element.android.features.login.impl.screens.waitlistscreen package io.element.android.features.login.impl.screens.waitlistscreen
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class WaitListState( data class WaitListState(
val appName: String, val appName: String,
val serverName: String, val serverName: String,
val loginAction: Async<SessionId>, val loginAction: AsyncData<SessionId>,
val eventSink: (WaitListEvents) -> Unit val eventSink: (WaitListEvents) -> Unit
) )

View file

@ -17,17 +17,17 @@
package io.element.android.features.login.impl.screens.waitlistscreen package io.element.android.features.login.impl.screens.waitlistscreen
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
open class WaitListStateProvider : PreviewParameterProvider<WaitListState> { open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
override val values: Sequence<WaitListState> override val values: Sequence<WaitListState>
get() = sequenceOf( get() = sequenceOf(
aWaitListState(loginAction = Async.Uninitialized), aWaitListState(loginAction = AsyncData.Uninitialized),
aWaitListState(loginAction = Async.Loading()), aWaitListState(loginAction = AsyncData.Loading()),
aWaitListState(loginAction = Async.Failure(Throwable("error"))), aWaitListState(loginAction = AsyncData.Failure(Throwable("error"))),
aWaitListState(loginAction = Async.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))), aWaitListState(loginAction = AsyncData.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
aWaitListState(loginAction = Async.Success(SessionId("@alice:element.io"))), aWaitListState(loginAction = AsyncData.Success(SessionId("@alice:element.io"))),
// Add other state here // Add other state here
) )
} }
@ -35,7 +35,7 @@ open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
fun aWaitListState( fun aWaitListState(
appName: String = "Element X", appName: String = "Element X",
serverName: String = "server.org", serverName: String = "server.org",
loginAction: Async<SessionId> = Async.Uninitialized, loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
) = WaitListState( ) = WaitListState(
appName = appName, appName = appName,
serverName = serverName, serverName = serverName,

View file

@ -33,7 +33,7 @@ import androidx.lifecycle.Lifecycle
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.isWaitListError import io.element.android.features.login.impl.error.isWaitListError
import io.element.android.features.login.impl.error.loginError import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -89,12 +89,12 @@ private fun WaitListContent(
) { ) {
val title = stringResource( val title = stringResource(
when (state.loginAction) { when (state.loginAction) {
is Async.Success -> R.string.screen_waitlist_title_success is AsyncData.Success -> R.string.screen_waitlist_title_success
else -> R.string.screen_waitlist_title else -> R.string.screen_waitlist_title
} }
) )
val subtitle = when (state.loginAction) { val subtitle = when (state.loginAction) {
is Async.Success -> stringResource( is AsyncData.Success -> stringResource(
id = R.string.screen_waitlist_message_success, id = R.string.screen_waitlist_message_success,
state.appName, state.appName,
) )
@ -122,7 +122,7 @@ private fun OverallContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
if (state.loginAction !is Async.Success) { if (state.loginAction !is AsyncData.Success) {
CompositionLocalProvider(LocalContentColor provides Color.Black) { CompositionLocalProvider(LocalContentColor provides Color.Black) {
TextButton( TextButton(
text = stringResource(CommonStrings.action_cancel), text = stringResource(CommonStrings.action_cancel),
@ -130,7 +130,7 @@ private fun OverallContent(
) )
} }
} }
if (state.loginAction is Async.Success) { if (state.loginAction is AsyncData.Success) {
Button( Button(
text = stringResource(id = CommonStrings.action_continue), text = stringResource(id = CommonStrings.action_continue),
onClick = { state.eventSink.invoke(WaitListEvents.Continue) }, onClick = { state.eventSink.invoke(WaitListEvents.Continue) },

View file

@ -22,7 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
@ -46,7 +46,7 @@ class ChangeServerPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -61,13 +61,13 @@ class ChangeServerPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
authenticationService.givenHomeserver(A_HOMESERVER) authenticationService.givenHomeserver(A_HOMESERVER)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.changeServerAction).isEqualTo(Async.Success(Unit)) assertThat(successState.changeServerAction).isEqualTo(AsyncData.Success(Unit))
} }
} }
@ -82,16 +82,16 @@ class ChangeServerPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
val failureState = awaitItem() val failureState = awaitItem()
assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java) assertThat(failureState.changeServerAction).isInstanceOf(AsyncData.Failure::class.java)
// Clear error // Clear error
failureState.eventSink.invoke(ChangeServerEvents.ClearError) failureState.eventSink.invoke(ChangeServerEvents.ClearError)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.changeServerAction).isEqualTo(Async.Uninitialized) assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
} }

View file

@ -23,7 +23,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
@ -49,7 +49,7 @@ class OidcPresenterTest {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA) assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA)
assertThat(initialState.requestState).isEqualTo(Async.Uninitialized) assertThat(initialState.requestState).isEqualTo(AsyncAction.Uninitialized)
} }
} }
@ -65,9 +65,9 @@ class OidcPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.Cancel) initialState.eventSink.invoke(OidcEvents.Cancel)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>()) assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.requestState).isEqualTo(Async.Success(Unit)) assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit))
} }
} }
@ -85,9 +85,9 @@ class OidcPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.Cancel) initialState.eventSink.invoke(OidcEvents.Cancel)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>()) assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.requestState).isEqualTo(Async.Failure<Unit>(A_THROWABLE)) assertThat(finalState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE))
// Note: in real life I do not think this can happen, and the app should not block the user. // Note: in real life I do not think this can happen, and the app should not block the user.
} }
} }
@ -104,9 +104,9 @@ class OidcPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack)) initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>()) assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.requestState).isEqualTo(Async.Success(Unit)) assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit))
} }
} }
@ -122,7 +122,7 @@ class OidcPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>()) assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
// In this case, no success, the session is created and the node get destroyed. // In this case, no success, the session is created and the node get destroyed.
} }
} }
@ -141,12 +141,12 @@ class OidcPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>()) assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.requestState).isEqualTo(Async.Failure<Unit>(A_THROWABLE)) assertThat(errorState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE))
errorState.eventSink.invoke(OidcEvents.ClearError) errorState.eventSink.invoke(OidcEvents.ClearError)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.requestState).isEqualTo(Async.Uninitialized) assertThat(finalState.requestState).isEqualTo(AsyncAction.Uninitialized)
} }
} }
} }

View file

@ -25,7 +25,7 @@ import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
@ -52,7 +52,7 @@ class ConfirmAccountProviderPresenterTest {
assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
assertThat(initialState.loginFlow).isEqualTo(Async.Uninitialized) assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -70,10 +70,10 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin) assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin)
} }
} }
@ -92,10 +92,10 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
} }
} }
@ -116,15 +116,15 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
authenticationService.givenOidcCancelError(A_THROWABLE) authenticationService.givenOidcCancelError(A_THROWABLE)
defaultOidcActionFlow.post(OidcAction.GoBack) defaultOidcActionFlow.post(OidcAction.GoBack)
val cancelFailureState = awaitItem() val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java) assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@ -144,14 +144,14 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
defaultOidcActionFlow.post(OidcAction.GoBack) defaultOidcActionFlow.post(OidcAction.GoBack)
val cancelFinalState = awaitItem() val cancelFinalState = awaitItem()
assertThat(cancelFinalState.loginFlow).isInstanceOf(Async.Uninitialized::class.java) assertThat(cancelFinalState.loginFlow).isInstanceOf(AsyncData.Uninitialized::class.java)
} }
} }
@ -171,17 +171,17 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
authenticationService.givenLoginError(A_THROWABLE) authenticationService.givenLoginError(A_THROWABLE)
defaultOidcActionFlow.post(OidcAction.Success("aUrl")) defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val cancelLoadingState = awaitItem() val cancelLoadingState = awaitItem()
assertThat(cancelLoadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(cancelLoadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val cancelFailureState = awaitItem() val cancelFailureState = awaitItem()
assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java) assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@ -205,15 +205,15 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse() assertThat(successState.submitEnabled).isFalse()
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse() assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
defaultOidcActionFlow.post(OidcAction.Success("aUrl")) defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val successSuccessState = awaitItem() val successSuccessState = awaitItem()
assertThat(successSuccessState.loginFlow).isInstanceOf(Async.Loading::class.java) assertThat(successSuccessState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value } waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
} }
} }
@ -233,7 +233,7 @@ class ConfirmAccountProviderPresenterTest {
skipItems(1) // Loading skipItems(1) // Loading
val failureState = awaitItem() val failureState = awaitItem()
assertThat(failureState.submitEnabled).isFalse() assertThat(failureState.submitEnabled).isFalse()
assertThat(failureState.loginFlow).isInstanceOf(Async.Failure::class.java) assertThat(failureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
} }
} }
@ -256,12 +256,12 @@ class ConfirmAccountProviderPresenterTest {
// Check an error was returned // Check an error was returned
val submittedState = awaitItem() val submittedState = awaitItem()
assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java) assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
// Assert the error is then cleared // Assert the error is then cleared
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
val clearedState = awaitItem() val clearedState = awaitItem()
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized) assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized)
} }
} }

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_PASSWORD
@ -57,7 +57,7 @@ class LoginPasswordPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized) assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.submitEnabled).isFalse() assertThat(initialState.submitEnabled).isFalse()
} }
} }
@ -110,9 +110,9 @@ class LoginPasswordPresenterTest {
val loginAndPasswordState = awaitItem() val loginAndPasswordState = awaitItem()
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem() val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem() val loggedInState = awaitItem()
assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID)) assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Success(A_SESSION_ID))
assertThat(loginUserStory.loginFlowIsDone.value).isTrue() assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
} }
} }
@ -139,9 +139,9 @@ class LoginPasswordPresenterTest {
authenticationService.givenLoginError(A_THROWABLE) authenticationService.givenLoginError(A_THROWABLE)
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem() val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem() val loggedInState = awaitItem()
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE)) assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
} }
} }
@ -167,14 +167,14 @@ class LoginPasswordPresenterTest {
authenticationService.givenLoginError(A_THROWABLE) authenticationService.givenLoginError(A_THROWABLE)
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem() val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem() val loggedInState = awaitItem()
// Check an error was returned // Check an error was returned
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE)) assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
// Assert the error is then cleared // Assert the error is then cleared
loggedInState.eventSink(LoginPasswordEvents.ClearError) loggedInState.eventSink(LoginPasswordEvents.ClearError)
val clearedState = awaitItem() val clearedState = awaitItem()
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized) assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
} }

View file

@ -27,7 +27,7 @@ import io.element.android.features.login.impl.resolver.network.FakeWellknownRequ
import io.element.android.features.login.impl.resolver.network.WellKnown import io.element.android.features.login.impl.resolver.network.WellKnown
import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig
import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -57,7 +57,7 @@ class SearchAccountProviderPresenterTest {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userInput).isEmpty() assertThat(initialState.userInput).isEmpty()
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -79,9 +79,9 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem() val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test") assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(Async.Uninitialized) assertThat(awaitItem().userInputResult).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -103,10 +103,10 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
val withInputState = awaitItem() val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("https://test.org") assertThat(withInputState.userInput).isEqualTo("https://test.org")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo( assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success( AsyncData.Success(
listOf( listOf(
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false) aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false)
) )
@ -138,10 +138,10 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem() val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test") assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo( assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success( AsyncData.Success(
listOf( listOf(
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false) aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false)
) )
@ -173,10 +173,10 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem() val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test") assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo( assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success( AsyncData.Success(
listOf( listOf(
aHomeserverData(homeserverUrl = "https://test.io") aHomeserverData(homeserverUrl = "https://test.io")
) )

View file

@ -22,7 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
@ -58,7 +58,7 @@ class WaitListPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo("Application Name") assertThat(initialState.appName).isEqualTo("Application Name")
assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL) assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL)
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized) assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -83,13 +83,13 @@ class WaitListPresenterTest {
expectNoEvents() expectNoEvents()
initialState.eventSink.invoke(WaitListEvents.AttemptLogin) initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
val submitState = awaitItem() val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE)) assertThat(errorState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
// Assert the error can be cleared // Assert the error can be cleared
errorState.eventSink(WaitListEvents.ClearError) errorState.eventSink(WaitListEvents.ClearError)
val clearedState = awaitItem() val clearedState = awaitItem()
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized) assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -113,9 +113,9 @@ class WaitListPresenterTest {
expectNoEvents() expectNoEvents()
initialState.eventSink.invoke(WaitListEvents.AttemptLogin) initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
val submitState = awaitItem() val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.loginAction).isEqualTo(Async.Success(A_USER_ID)) assertThat(successState.loginAction).isEqualTo(AsyncData.Success(A_USER_ID))
assertThat(loginUserStory.loginFlowIsDone.value).isFalse() assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
successState.eventSink.invoke(WaitListEvents.Continue) successState.eventSink.invoke(WaitListEvents.Continue)
assertThat(loginUserStory.loginFlowIsDone.value).isTrue() assertThat(loginUserStory.loginFlowIsDone.value).isTrue()

View file

@ -16,11 +16,10 @@
package io.element.android.features.logout.api.direct package io.element.android.features.logout.api.direct
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
data class DirectLogoutState( data class DirectLogoutState(
val canDoDirectSignOut: Boolean, val canDoDirectSignOut: Boolean,
val showConfirmationDialog: Boolean, val logoutAction: AsyncAction<String?>,
val logoutAction: Async<String?>,
val eventSink: (DirectLogoutEvents) -> Unit, val eventSink: (DirectLogoutEvents) -> Unit,
) )

View file

@ -25,7 +25,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.core.bool.orTrue
@ -50,8 +51,8 @@ class LogoutPresenter @Inject constructor(
@Composable @Composable
override fun present(): LogoutState { override fun present(): LogoutState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<String?>> = remember { val logoutAction: MutableState<AsyncAction<String?>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncAction.Uninitialized)
} }
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
@ -66,7 +67,6 @@ class LogoutPresenter @Inject constructor(
} }
.collectAsState(initial = BackupUploadState.Unknown) .collectAsState(initial = BackupUploadState.Unknown)
var showLogoutDialog by remember { mutableStateOf(false) }
var isLastSession by remember { mutableStateOf(false) } var isLastSession by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
@ -75,8 +75,8 @@ class LogoutPresenter @Inject constructor(
val backupState by encryptionService.backupStateStateFlow.collectAsState() val backupState by encryptionService.backupStateStateFlow.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val doesBackupExistOnServerAction: MutableState<Async<Boolean>> = remember { val doesBackupExistOnServerAction: MutableState<AsyncData<Boolean>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncData.Uninitialized)
} }
LaunchedEffect(backupState) { LaunchedEffect(backupState) {
@ -88,16 +88,14 @@ class LogoutPresenter @Inject constructor(
fun handleEvents(event: LogoutEvents) { fun handleEvents(event: LogoutEvents) {
when (event) { when (event) {
is LogoutEvents.Logout -> { is LogoutEvents.Logout -> {
if (showLogoutDialog || event.ignoreSdkError) { if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
showLogoutDialog = false
localCoroutineScope.logout(logoutAction, event.ignoreSdkError) localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else { } else {
showLogoutDialog = true logoutAction.value = AsyncAction.Confirming
} }
} }
LogoutEvents.CloseDialogs -> { LogoutEvents.CloseDialogs -> {
logoutAction.value = Async.Uninitialized logoutAction.value = AsyncAction.Uninitialized
showLogoutDialog = false
} }
} }
} }
@ -108,20 +106,19 @@ class LogoutPresenter @Inject constructor(
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(), doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
recoveryState = recoveryState, recoveryState = recoveryState,
backupUploadState = backupUploadState, backupUploadState = backupUploadState,
showConfirmationDialog = showLogoutDialog,
logoutAction = logoutAction.value, logoutAction = logoutAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.getKeyBackupStatus(action: MutableState<Async<Boolean>>) = launch { private fun CoroutineScope.getKeyBackupStatus(action: MutableState<AsyncData<Boolean>>) = launch {
suspend { suspend {
encryptionService.doesBackupExistOnServer().getOrThrow() encryptionService.doesBackupExistOnServer().getOrThrow()
}.runCatchingUpdatingState(action) }.runCatchingUpdatingState(action)
} }
private fun CoroutineScope.logout( private fun CoroutineScope.logout(
logoutAction: MutableState<Async<String?>>, logoutAction: MutableState<AsyncAction<String?>>,
ignoreSdkError: Boolean, ignoreSdkError: Boolean,
) = launch { ) = launch {
suspend { suspend {

View file

@ -16,7 +16,7 @@
package io.element.android.features.logout.impl package io.element.android.features.logout.impl
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
@ -27,7 +27,6 @@ data class LogoutState(
val doesBackupExistOnServer: Boolean, val doesBackupExistOnServer: Boolean,
val recoveryState: RecoveryState, val recoveryState: RecoveryState,
val backupUploadState: BackupUploadState, val backupUploadState: BackupUploadState,
val showConfirmationDialog: Boolean, val logoutAction: AsyncAction<String?>,
val logoutAction: Async<String?>,
val eventSink: (LogoutEvents) -> Unit, val eventSink: (LogoutEvents) -> Unit,
) )

View file

@ -17,7 +17,7 @@
package io.element.android.features.logout.impl package io.element.android.features.logout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
@ -30,9 +30,9 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
aLogoutState(isLastSession = true), aLogoutState(isLastSession = true),
aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)), aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done), aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done),
aLogoutState(showConfirmationDialog = true), aLogoutState(logoutAction = AsyncAction.Confirming),
aLogoutState(logoutAction = Async.Loading()), aLogoutState(logoutAction = AsyncAction.Loading),
aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))), aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))), aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
// Last session no recovery // Last session no recovery
aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED), aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED),
@ -47,15 +47,13 @@ fun aLogoutState(
doesBackupExistOnServer: Boolean = true, doesBackupExistOnServer: Boolean = true,
recoveryState: RecoveryState = RecoveryState.ENABLED, recoveryState: RecoveryState = RecoveryState.ENABLED,
backupUploadState: BackupUploadState = BackupUploadState.Unknown, backupUploadState: BackupUploadState = BackupUploadState.Unknown,
showConfirmationDialog: Boolean = false, logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
logoutAction: Async<String?> = Async.Uninitialized,
) = LogoutState( ) = LogoutState(
isLastSession = isLastSession, isLastSession = isLastSession,
backupState = backupState, backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServer, doesBackupExistOnServer = doesBackupExistOnServer,
recoveryState = recoveryState, recoveryState = recoveryState,
backupUploadState = backupUploadState, backupUploadState = backupUploadState,
showConfirmationDialog = showConfirmationDialog,
logoutAction = logoutAction, logoutAction = logoutAction,
eventSink = {} eventSink = {}
) )

View file

@ -32,8 +32,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.features.logout.impl.ui.LogoutActionDialog import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
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
@ -79,20 +78,11 @@ fun LogoutView(
}, },
) )
// Log out confirmation dialog
if (state.showConfirmationDialog) {
LogoutConfirmationDialog(
onSubmitClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
onDismiss = {
eventSink(LogoutEvents.CloseDialogs)
}
)
}
LogoutActionDialog( LogoutActionDialog(
state.logoutAction, state.logoutAction,
onConfirmClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClicked = { onForceLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true)) eventSink(LogoutEvents.Logout(ignoreSdkError = true))
}, },
@ -148,13 +138,13 @@ private fun ColumnScope.Buttons(
) )
} }
val signOutSubmitRes = when { val signOutSubmitRes = when {
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content logoutAction is AsyncAction.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout else -> CommonStrings.action_signout
} }
Button( Button(
text = stringResource(id = signOutSubmitRes), text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading, showProgress = logoutAction is AsyncAction.Loading,
destructive = true, destructive = true,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -30,7 +30,7 @@ import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -54,8 +54,8 @@ class DefaultDirectLogoutPresenter @Inject constructor(
override fun present(): DirectLogoutState { override fun present(): DirectLogoutState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<String?>> = remember { val logoutAction: MutableState<AsyncAction<String?>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncAction.Uninitialized)
} }
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
@ -70,7 +70,6 @@ class DefaultDirectLogoutPresenter @Inject constructor(
} }
.collectAsState(initial = BackupUploadState.Unknown) .collectAsState(initial = BackupUploadState.Unknown)
var showLogoutDialog by remember { mutableStateOf(false) }
var isLastSession by remember { mutableStateOf(false) } var isLastSession by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
@ -79,16 +78,14 @@ class DefaultDirectLogoutPresenter @Inject constructor(
fun handleEvents(event: DirectLogoutEvents) { fun handleEvents(event: DirectLogoutEvents) {
when (event) { when (event) {
is DirectLogoutEvents.Logout -> { is DirectLogoutEvents.Logout -> {
if (showLogoutDialog || event.ignoreSdkError) { if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
showLogoutDialog = false
localCoroutineScope.logout(logoutAction, event.ignoreSdkError) localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else { } else {
showLogoutDialog = true logoutAction.value = AsyncAction.Confirming
} }
} }
DirectLogoutEvents.CloseDialogs -> { DirectLogoutEvents.CloseDialogs -> {
logoutAction.value = Async.Uninitialized logoutAction.value = AsyncAction.Uninitialized
showLogoutDialog = false
} }
} }
} }
@ -96,14 +93,13 @@ class DefaultDirectLogoutPresenter @Inject constructor(
return DirectLogoutState( return DirectLogoutState(
canDoDirectSignOut = !isLastSession && canDoDirectSignOut = !isLastSession &&
!backupUploadState.isBackingUp(), !backupUploadState.isBackingUp(),
showConfirmationDialog = showLogoutDialog,
logoutAction = logoutAction.value, logoutAction = logoutAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.logout( private fun CoroutineScope.logout(
logoutAction: MutableState<Async<String?>>, logoutAction: MutableState<AsyncAction<String?>>,
ignoreSdkError: Boolean, ignoreSdkError: Boolean,
) = launch { ) = launch {
suspend { suspend {

View file

@ -22,7 +22,6 @@ import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.logout.impl.ui.LogoutActionDialog import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import javax.inject.Inject import javax.inject.Inject
@ -34,20 +33,11 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
onSuccessLogout: (logoutUrlResult: String?) -> Unit, onSuccessLogout: (logoutUrlResult: String?) -> Unit,
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
// Log out confirmation dialog
if (state.showConfirmationDialog) {
LogoutConfirmationDialog(
onSubmitClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onDismiss = {
eventSink(DirectLogoutEvents.CloseDialogs)
}
)
}
LogoutActionDialog( LogoutActionDialog(
state.logoutAction, state.logoutAction,
onConfirmClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClicked = { onForceLogoutClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true)) eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
}, },

View file

@ -20,22 +20,30 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.logout.impl.R import io.element.android.features.logout.impl.R
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun LogoutActionDialog( fun LogoutActionDialog(
state: Async<String?>, state: AsyncAction<String?>,
onConfirmClicked: () -> Unit,
onForceLogoutClicked: () -> Unit, onForceLogoutClicked: () -> Unit,
onDismissError: () -> Unit, onDismissError: () -> Unit, // TODO Rename
onSuccessLogout: (String?) -> Unit, onSuccessLogout: (String?) -> Unit,
) { ) {
when (state) { when (state) {
is Async.Loading -> AsyncAction.Uninitialized ->
Unit
AsyncAction.Confirming ->
LogoutConfirmationDialog(
onSubmitClicked = onConfirmClicked,
onDismiss = onDismissError
)
is AsyncAction.Loading ->
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
is Async.Failure -> is AsyncAction.Failure ->
RetryDialog( RetryDialog(
title = stringResource(id = CommonStrings.dialog_title_error), title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.error_unknown), content = stringResource(id = CommonStrings.error_unknown),
@ -43,9 +51,7 @@ fun LogoutActionDialog(
onRetry = onForceLogoutClicked, onRetry = onForceLogoutClicked,
onDismiss = onDismissError, onDismiss = onDismissError,
) )
Async.Uninitialized -> is AsyncAction.Success ->
Unit
is Async.Success ->
LaunchedEffect(state) { LaunchedEffect(state) {
onSuccessLogout(state.data) onSuccessLogout(state.data)
} }

View file

@ -18,9 +18,10 @@ package io.element.android.features.logout.impl
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -32,7 +33,6 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -50,14 +50,13 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitFirstItem()
assertThat(initialState.isLastSession).isFalse() assertThat(initialState.isLastSession).isFalse()
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN) assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
assertThat(initialState.doesBackupExistOnServer).isTrue() assertThat(initialState.doesBackupExistOnServer).isTrue()
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN) assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
} }
} }
@ -75,8 +74,7 @@ class LogoutPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isLastSession).isTrue() assertThat(initialState.isLastSession).isTrue()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
} }
} }
@ -101,8 +99,7 @@ class LogoutPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isLastSession).isFalse() assertThat(initialState.isLastSession).isFalse()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
skipItems(1) skipItems(1)
val waitingState = awaitItem() val waitingState = awaitItem()
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting) assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
@ -120,13 +117,13 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
initialState.eventSink.invoke(LogoutEvents.CloseDialogs) initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.showConfirmationDialog).isFalse() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
} }
} }
@ -136,17 +133,15 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
} }
} }
@ -161,22 +156,18 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
skipItems(1)
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<LogoutState>(A_THROWABLE)) assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
errorState.eventSink.invoke(LogoutEvents.CloseDialogs) errorState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized) assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
} }
} }
@ -191,28 +182,28 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
skipItems(1)
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<LogoutState>(A_THROWABLE)) assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true)) errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true))
val loadingState2 = awaitItem() val loadingState2 = awaitItem()
assertThat(loadingState2.showConfirmationDialog).isFalse() assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
} }
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(2)
return awaitItem()
}
private fun createLogoutPresenter( private fun createLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),

View file

@ -18,11 +18,11 @@ package io.element.android.features.logout.impl.direct
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -49,10 +48,9 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isTrue() assertThat(initialState.canDoDirectSignOut).isTrue()
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
} }
} }
@ -69,8 +67,7 @@ class DefaultDirectLogoutPresenterTest {
skipItems(2) skipItems(2)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
} }
} }
@ -91,8 +88,7 @@ class DefaultDirectLogoutPresenterTest {
skipItems(2) skipItems(2)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
} }
} }
@ -102,13 +98,13 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.showConfirmationDialog).isFalse() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
} }
} }
@ -118,17 +114,15 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
} }
} }
@ -143,21 +137,18 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<DirectLogoutState>(A_THROWABLE)) assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized) assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
} }
} }
@ -172,27 +163,28 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<DirectLogoutState>(A_THROWABLE)) assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true)) errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true))
val loadingState2 = awaitItem() val loadingState2 = awaitItem()
assertThat(loadingState2.showConfirmationDialog).isFalse() assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java)
val successState = awaitItem() val successState = awaitItem()
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
} }
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
private fun createDefaultDirectLogoutPresenter( private fun createDefaultDirectLogoutPresenter(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),

View file

@ -65,7 +65,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.PreferencesStore import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@ -141,11 +141,11 @@ class MessagesPresenter @AssistedInject constructor(
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value) val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value) val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value)
val roomName: Async<String> by remember { val roomName: AsyncData<String> by remember {
derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized } derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
} }
val roomAvatar: Async<AvatarData> by remember { val roomAvatar: AsyncData<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized } derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
} }
var hasDismissedInviteDialog by rememberSaveable { var hasDismissedInviteDialog by rememberSaveable {
@ -162,7 +162,7 @@ class MessagesPresenter @AssistedInject constructor(
} }
} }
val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) } val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) { LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
@ -279,8 +279,8 @@ class MessagesPresenter @AssistedInject constructor(
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) { private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) {
inviteProgress.value = Async.Loading() inviteProgress.value = AsyncData.Loading()
runCatching { runCatching {
room.updateMembers() room.updateMembers()
@ -296,10 +296,10 @@ class MessagesPresenter @AssistedInject constructor(
}.getOrThrow() }.getOrThrow()
}.fold( }.fold(
onSuccess = { onSuccess = {
inviteProgress.value = Async.Success(Unit) inviteProgress.value = AsyncData.Success(Unit)
}, },
onFailure = { onFailure = {
inviteProgress.value = Async.Failure(it) inviteProgress.value = AsyncData.Failure(it)
} }
) )
} }

View file

@ -25,7 +25,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -33,8 +33,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
@Immutable @Immutable
data class MessagesState( data class MessagesState(
val roomId: RoomId, val roomId: RoomId,
val roomName: Async<String>, val roomName: AsyncData<String>,
val roomAvatar: Async<AvatarData>, val roomAvatar: AsyncData<AvatarData>,
val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToRedact: Boolean, val userHasPermissionToRedact: Boolean,
val userHasPermissionToSendReaction: Boolean, val userHasPermissionToSendReaction: Boolean,
@ -48,7 +48,7 @@ data class MessagesState(
val readReceiptBottomSheetState: ReadReceiptBottomSheetState, val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
val hasNetworkConnection: Boolean, val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val inviteProgress: Async<Unit>, val inviteProgress: AsyncData<Unit>,
val showReinvitePrompt: Boolean, val showReinvitePrompt: Boolean,
val enableTextFormatting: Boolean, val enableTextFormatting: Boolean,
val enableVoiceMessages: Boolean, val enableVoiceMessages: Boolean,

View file

@ -29,7 +29,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -47,8 +47,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
aMessagesState().copy(userHasPermissionToSendMessage = false), aMessagesState().copy(userHasPermissionToSendMessage = false),
aMessagesState().copy(showReinvitePrompt = true), aMessagesState().copy(showReinvitePrompt = true),
aMessagesState().copy( aMessagesState().copy(
roomName = Async.Uninitialized, roomName = AsyncData.Uninitialized,
roomAvatar = Async.Uninitialized, roomAvatar = AsyncData.Uninitialized,
), ),
aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)), aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)),
aMessagesState().copy( aMessagesState().copy(
@ -83,8 +83,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
fun aMessagesState() = MessagesState( fun aMessagesState() = MessagesState(
roomId = RoomId("!id:domain"), roomId = RoomId("!id:domain"),
roomName = Async.Success("Room name"), roomName = AsyncData.Success("Room name"),
roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)), roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage = true, userHasPermissionToSendMessage = true,
userHasPermissionToRedact = false, userHasPermissionToRedact = false,
userHasPermissionToSendReaction = true, userHasPermissionToSendReaction = true,
@ -117,7 +117,7 @@ fun aMessagesState() = MessagesState(
), ),
hasNetworkConnection = true, hasNetworkConnection = true,
snackbarMessage = null, snackbarMessage = null,
inviteProgress = Async.Uninitialized, inviteProgress = AsyncData.Uninitialized,
showReinvitePrompt = false, showReinvitePrompt = false,
enableTextFormatting = true, enableTextFormatting = true,
enableVoiceMessages = true, enableVoiceMessages = true,

View file

@ -25,7 +25,7 @@ import androidx.compose.runtime.remember
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -48,7 +48,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
fun create(eventId: String): ForwardMessagesPresenter fun create(eventId: String): ForwardMessagesPresenter
} }
private val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = mutableStateOf(Async.Uninitialized) private val forwardingActionState: MutableState<AsyncData<ImmutableList<RoomId>>> = mutableStateOf(AsyncData.Uninitialized)
fun onRoomSelected(roomIds: List<RoomId>) { fun onRoomSelected(roomIds: List<RoomId>) {
matrixCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState) matrixCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
@ -62,13 +62,13 @@ class ForwardMessagesPresenter @AssistedInject constructor(
fun handleEvents(event: ForwardMessagesEvents) { fun handleEvents(event: ForwardMessagesEvents) {
when (event) { when (event) {
ForwardMessagesEvents.ClearError -> forwardingActionState.value = Async.Uninitialized ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncData.Uninitialized
} }
} }
return ForwardMessagesState( return ForwardMessagesState(
isForwarding = forwardingActionState.value.isLoading(), isForwarding = forwardingActionState.value.isLoading(),
error = (forwardingActionState.value as? Async.Failure)?.error, error = (forwardingActionState.value as? AsyncData.Failure)?.error,
forwardingSucceeded = forwardingSucceeded, forwardingSucceeded = forwardingSucceeded,
eventSink = { handleEvents(it) } eventSink = { handleEvents(it) }
) )
@ -77,12 +77,12 @@ class ForwardMessagesPresenter @AssistedInject constructor(
private fun CoroutineScope.forwardEvent( private fun CoroutineScope.forwardEvent(
eventId: EventId, eventId: EventId,
roomIds: ImmutableList<RoomId>, roomIds: ImmutableList<RoomId>,
isForwardMessagesState: MutableState<Async<ImmutableList<RoomId>>>, isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>,
) = launch { ) = launch {
isForwardMessagesState.value = Async.Loading() isForwardMessagesState.value = AsyncData.Loading()
room.forwardEvent(eventId, roomIds).fold( room.forwardEvent(eventId, roomIds).fold(
{ isForwardMessagesState.value = Async.Success(roomIds) }, { isForwardMessagesState.value = AsyncData.Success(roomIds) },
{ isForwardMessagesState.value = Async.Failure(it) } { isForwardMessagesState.value = AsyncData.Failure(it) }
) )
} }
} }

View file

@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async 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.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -60,14 +60,14 @@ class ReportMessagePresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var reason by rememberSaveable { mutableStateOf("") } var reason by rememberSaveable { mutableStateOf("") }
var blockUser by rememberSaveable { mutableStateOf(false) } var blockUser by rememberSaveable { mutableStateOf(false) }
var result: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } var result: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvents(event: ReportMessageEvents) { fun handleEvents(event: ReportMessageEvents) {
when (event) { when (event) {
is ReportMessageEvents.UpdateReason -> reason = event.reason is ReportMessageEvents.UpdateReason -> reason = event.reason
ReportMessageEvents.ToggleBlockUser -> blockUser = !blockUser ReportMessageEvents.ToggleBlockUser -> blockUser = !blockUser
ReportMessageEvents.Report -> coroutineScope.report(inputs.eventId, inputs.senderId, reason, blockUser, result) ReportMessageEvents.Report -> coroutineScope.report(inputs.eventId, inputs.senderId, reason, blockUser, result)
ReportMessageEvents.ClearError -> result.value = Async.Uninitialized ReportMessageEvents.ClearError -> result.value = AsyncAction.Uninitialized
} }
} }
@ -84,7 +84,7 @@ class ReportMessagePresenter @AssistedInject constructor(
userId: UserId, userId: UserId,
reason: String, reason: String,
blockUser: Boolean, blockUser: Boolean,
result: MutableState<Async<Unit>>, result: MutableState<AsyncAction<Unit>>,
) = launch { ) = launch {
result.runUpdatingState { result.runUpdatingState {
val userIdToBlock = userId.takeIf { blockUser } val userIdToBlock = userId.takeIf { blockUser }

View file

@ -16,11 +16,11 @@
package io.element.android.features.messages.impl.report package io.element.android.features.messages.impl.report
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
data class ReportMessageState( data class ReportMessageState(
val reason: String, val reason: String,
val blockUser: Boolean, val blockUser: Boolean,
val result: Async<Unit>, val result: AsyncAction<Unit>,
val eventSink: (ReportMessageEvents) -> Unit val eventSink: (ReportMessageEvents) -> Unit
) )

View file

@ -17,7 +17,7 @@
package io.element.android.features.messages.impl.report package io.element.android.features.messages.impl.report
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageState> { open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageState> {
override val values: Sequence<ReportMessageState> override val values: Sequence<ReportMessageState>
@ -25,9 +25,9 @@ open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageSt
aReportMessageState(), aReportMessageState(),
aReportMessageState(reason = "This user is making the chat very toxic."), aReportMessageState(reason = "This user is making the chat very toxic."),
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true), aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true),
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Loading()), aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = AsyncAction.Loading),
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Failure(Throwable("error"))), aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = AsyncAction.Failure(Throwable("error"))),
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Success(Unit)), aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = AsyncAction.Success(Unit)),
// Add other states here // Add other states here
) )
} }
@ -35,7 +35,7 @@ open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageSt
fun aReportMessageState( fun aReportMessageState(
reason: String = "", reason: String = "",
blockUser: Boolean = false, blockUser: Boolean = false,
result: Async<Unit> = Async.Uninitialized, result: AsyncAction<Unit> = AsyncAction.Uninitialized,
) = ReportMessageState( ) = ReportMessageState(
reason = reason, reason = reason,
blockUser = blockUser, blockUser = blockUser,

View file

@ -41,8 +41,8 @@ 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.features.messages.impl.R import io.element.android.features.messages.impl.R
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.async.AsyncActionView
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
@ -62,10 +62,10 @@ fun ReportMessageView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val isSending = state.result is Async.Loading val isSending = state.result is AsyncAction.Loading
AsyncView( AsyncActionView(
async = state.result, async = state.result,
showProgressDialog = false, progressDialog = {},
onSuccess = { onBackClicked() }, onSuccess = { onBackClicked() },
errorMessage = { stringResource(CommonStrings.error_unknown) }, errorMessage = { stringResource(CommonStrings.error_unknown) },
onErrorDismiss = { state.eventSink(ReportMessageEvents.ClearError) } onErrorDismiss = { state.eventSink(ReportMessageEvents.ClearError) }

View file

@ -33,7 +33,7 @@ import io.element.android.features.messages.impl.timeline.di.TimelineItemEventCo
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ -71,7 +71,7 @@ class VoiceMessagePresenter @AssistedInject constructor(
body = content.body, body = content.body,
) )
private val play = mutableStateOf<Async<Unit>>(Async.Uninitialized) private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
@Composable @Composable
override fun present(): VoiceMessageState { override fun present(): VoiceMessageState {
@ -91,8 +91,8 @@ class VoiceMessagePresenter @AssistedInject constructor(
when { when {
content.eventId == null -> VoiceMessageState.Button.Disabled content.eventId == null -> VoiceMessageState.Button.Disabled
playerState.isPlaying -> VoiceMessageState.Button.Pause playerState.isPlaying -> VoiceMessageState.Button.Pause
play.value is Async.Loading -> VoiceMessageState.Button.Downloading play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
play.value is Async.Failure -> VoiceMessageState.Button.Retry play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
else -> VoiceMessageState.Button.Play else -> VoiceMessageState.Button.Play
} }
} }

View file

@ -51,7 +51,7 @@ import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -117,14 +117,14 @@ class MessagesPresenterTest {
}.test { }.test {
val initialState = consumeItemsUntilTimeout().last() val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID) assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
assertThat(initialState.roomName).isEqualTo(Async.Success("")) assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
assertThat(initialState.roomAvatar) assertThat(initialState.roomAvatar)
.isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))) .isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
assertThat(initialState.userHasPermissionToSendMessage).isTrue() assertThat(initialState.userHasPermissionToSendMessage).isTrue()
assertThat(initialState.userHasPermissionToRedact).isFalse() assertThat(initialState.userHasPermissionToRedact).isFalse()
assertThat(initialState.hasNetworkConnection).isTrue() assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(Async.Uninitialized) assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
} }
} }

View file

@ -20,7 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
@ -45,7 +45,7 @@ class ReportMessagePresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.reason).isEmpty() assertThat(initialState.reason).isEmpty()
assertThat(initialState.blockUser).isFalse() assertThat(initialState.blockUser).isFalse()
assertThat(initialState.result).isInstanceOf(Async.Uninitialized::class.java) assertThat(initialState.result).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
@ -91,8 +91,8 @@ class ReportMessagePresenterTests {
initialState.eventSink(ReportMessageEvents.ToggleBlockUser) initialState.eventSink(ReportMessageEvents.ToggleBlockUser)
skipItems(1) skipItems(1)
initialState.eventSink(ReportMessageEvents.Report) initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java) assertThat(awaitItem().result).isInstanceOf(AsyncAction.Success::class.java)
assertThat(room.reportedContentCount).isEqualTo(1) assertThat(room.reportedContentCount).isEqualTo(1)
} }
} }
@ -106,8 +106,8 @@ class ReportMessagePresenterTests {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ReportMessageEvents.Report) initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java) assertThat(awaitItem().result).isInstanceOf(AsyncAction.Success::class.java)
assertThat(room.reportedContentCount).isEqualTo(1) assertThat(room.reportedContentCount).isEqualTo(1)
} }
} }
@ -123,13 +123,13 @@ class ReportMessagePresenterTests {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ReportMessageEvents.Report) initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
val resultState = awaitItem() val resultState = awaitItem()
assertThat(resultState.result).isInstanceOf(Async.Failure::class.java) assertThat(resultState.result).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(room.reportedContentCount).isEqualTo(1) assertThat(room.reportedContentCount).isEqualTo(1)
resultState.eventSink(ReportMessageEvents.ClearError) resultState.eventSink(ReportMessageEvents.ClearError)
assertThat(awaitItem().result).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().result).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }

View file

@ -32,7 +32,7 @@ import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
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.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
@ -65,10 +65,10 @@ class DeveloperSettingsPresenter @Inject constructor(
mutableStateMapOf<String, Boolean>() mutableStateMapOf<String, Boolean>()
} }
val cacheSize = remember { val cacheSize = remember {
mutableStateOf<Async<String>>(Async.Uninitialized) mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized)
} }
val clearCacheAction = remember { val clearCacheAction = remember {
mutableStateOf<Async<Unit>>(Async.Uninitialized) mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
} }
val customElementCallBaseUrl by preferencesStore val customElementCallBaseUrl by preferencesStore
.getCustomElementCallBaseUrlFlow() .getCustomElementCallBaseUrlFlow()
@ -154,13 +154,13 @@ class DeveloperSettingsPresenter @Inject constructor(
} }
} }
private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<Async<String>>) = launch { private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<AsyncData<String>>) = launch {
suspend { suspend {
computeCacheSizeUseCase() computeCacheSizeUseCase()
}.runCatchingUpdatingState(cacheSize) }.runCatchingUpdatingState(cacheSize)
} }
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncData<Unit>>) = launch {
suspend { suspend {
clearCacheUseCase() clearCacheUseCase()
}.runCatchingUpdatingState(clearCacheAction) }.runCatchingUpdatingState(clearCacheAction)

View file

@ -17,15 +17,15 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class DeveloperSettingsState( data class DeveloperSettingsState(
val features: ImmutableList<FeatureUiModel>, val features: ImmutableList<FeatureUiModel>,
val cacheSize: Async<String>, val cacheSize: AsyncData<String>,
val rageshakeState: RageshakePreferencesState, val rageshakeState: RageshakePreferencesState,
val clearCacheAction: Async<Unit>, val clearCacheAction: AsyncData<Unit>,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState, val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val eventSink: (DeveloperSettingsEvents) -> Unit val eventSink: (DeveloperSettingsEvents) -> Unit
) )

View file

@ -18,14 +18,14 @@ package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> { open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> {
override val values: Sequence<DeveloperSettingsState> override val values: Sequence<DeveloperSettingsState>
get() = sequenceOf( get() = sequenceOf(
aDeveloperSettingsState(), aDeveloperSettingsState(),
aDeveloperSettingsState().copy(clearCacheAction = Async.Loading()), aDeveloperSettingsState().copy(clearCacheAction = AsyncData.Loading()),
aDeveloperSettingsState().copy( aDeveloperSettingsState().copy(
customElementCallBaseUrlState = CustomElementCallBaseUrlState( customElementCallBaseUrlState = CustomElementCallBaseUrlState(
baseUrl = "https://call.element.ahoy", baseUrl = "https://call.element.ahoy",
@ -39,8 +39,8 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
fun aDeveloperSettingsState() = DeveloperSettingsState( fun aDeveloperSettingsState() = DeveloperSettingsState(
features = aFeatureUiModelList(), features = aFeatureUiModelList(),
rageshakeState = aRageshakePreferencesState(), rageshakeState = aRageshakePreferencesState(),
cacheSize = Async.Success("1.2 MB"), cacheSize = AsyncData.Success("1.2 MB"),
clearCacheAction = Async.Uninitialized, clearCacheAction = AsyncData.Uninitialized,
customElementCallBaseUrlState = CustomElementCallBaseUrlState(baseUrl = null, defaultUrl = "https://call.element.io", validator = { true }), customElementCallBaseUrlState = CustomElementCallBaseUrlState(baseUrl = null, defaultUrl = "https://call.element.io", validator = { true }),
eventSink = {} eventSink = {}
) )

View file

@ -23,7 +23,7 @@ import androidx.compose.runtime.collectAsState
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.rememberCoroutineScope
import io.element.android.libraries.architecture.Async 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.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -52,7 +52,7 @@ class NotificationSettingsPresenter @Inject constructor(
val systemNotificationsEnabled: MutableState<Boolean> = remember { val systemNotificationsEnabled: MutableState<Boolean> = remember {
mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled()) mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled())
} }
val changeNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val appNotificationsEnabled = userPushStore val appNotificationsEnabled = userPushStore
@ -87,7 +87,7 @@ class NotificationSettingsPresenter @Inject constructor(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> { NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> {
systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled() systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled()
} }
NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = Async.Uninitialized NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized
} }
} }
@ -119,7 +119,7 @@ class NotificationSettingsPresenter @Inject constructor(
val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow() val oneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = true).getOrThrow()
val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow() val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow()
if(groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) { if (groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) {
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false) target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
return@launch return@launch
} }
@ -168,19 +168,19 @@ class NotificationSettingsPresenter @Inject constructor(
) )
} }
private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean, action: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean, action: MutableState<AsyncAction<Unit>>) = launch {
suspend { suspend {
notificationSettingsService.setRoomMentionEnabled(enabled).getOrThrow() notificationSettingsService.setRoomMentionEnabled(enabled).getOrThrow()
}.runCatchingUpdatingState(action) }.runCatchingUpdatingState(action)
} }
private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean, action: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean, action: MutableState<AsyncAction<Unit>>) = launch {
suspend { suspend {
notificationSettingsService.setCallEnabled(enabled).getOrThrow() notificationSettingsService.setCallEnabled(enabled).getOrThrow()
}.runCatchingUpdatingState(action) }.runCatchingUpdatingState(action)
} }
private fun CoroutineScope.setInviteForMeNotificationsEnabled(enabled: Boolean, action: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.setInviteForMeNotificationsEnabled(enabled: Boolean, action: MutableState<AsyncAction<Unit>>) = launch {
suspend { suspend {
notificationSettingsService.setInviteForMeEnabled(enabled).getOrThrow() notificationSettingsService.setInviteForMeEnabled(enabled).getOrThrow()
}.runCatchingUpdatingState(action) }.runCatchingUpdatingState(action)

View file

@ -17,18 +17,18 @@
package io.element.android.features.preferences.impl.notifications package io.element.android.features.preferences.impl.notifications
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@Immutable @Immutable
data class NotificationSettingsState( data class NotificationSettingsState(
val matrixSettings: MatrixSettings, val matrixSettings: MatrixSettings,
val appSettings: AppSettings, val appSettings: AppSettings,
val changeNotificationSettingAction: Async<Unit>, val changeNotificationSettingAction: AsyncAction<Unit>,
val eventSink: (NotificationSettingsEvents) -> Unit, val eventSink: (NotificationSettingsEvents) -> Unit,
) { ) {
sealed interface MatrixSettings { sealed interface MatrixSettings {
data object Uninitialized : MatrixSettings data object Uninitialized : MatrixSettings
data class Valid( data class Valid(
val atRoomNotificationsEnabled: Boolean, val atRoomNotificationsEnabled: Boolean,
val callNotificationsEnabled: Boolean, val callNotificationsEnabled: Boolean,
@ -39,7 +39,7 @@ data class NotificationSettingsState(
data class Invalid( data class Invalid(
val fixFailed: Boolean val fixFailed: Boolean
) : MatrixSettings ) : MatrixSettings
} }
data class AppSettings( data class AppSettings(

View file

@ -17,20 +17,20 @@
package io.element.android.features.preferences.impl.notifications package io.element.android.features.preferences.impl.notifications
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
open class NotificationSettingsStateProvider : PreviewParameterProvider<NotificationSettingsState> { open class NotificationSettingsStateProvider : PreviewParameterProvider<NotificationSettingsState> {
override val values: Sequence<NotificationSettingsState> override val values: Sequence<NotificationSettingsState>
get() = sequenceOf( get() = sequenceOf(
aNotificationSettingsState(), aNotificationSettingsState(),
aNotificationSettingsState(changeNotificationSettingAction = Async.Loading(Unit)), aNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
aNotificationSettingsState(changeNotificationSettingAction = Async.Failure(Throwable("error"))), aNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))),
) )
} }
fun aNotificationSettingsState( fun aNotificationSettingsState(
changeNotificationSettingAction: Async<Unit> = Async.Uninitialized, changeNotificationSettingAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
) = NotificationSettingsState( ) = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Valid( matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = true, atRoomNotificationsEnabled = true,

View file

@ -27,7 +27,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferencePage
@ -80,7 +80,7 @@ fun NotificationSettingsView(
onInviteForMeNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) }, onInviteForMeNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) },
) )
} }
AsyncView( AsyncActionView(
async = state.changeNotificationSettingAction, async = state.changeNotificationSettingAction,
errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) }, errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) },
onErrorDismiss = { state.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) }, onErrorDismiss = { state.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) },

View file

@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async 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.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -55,6 +55,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
interface Factory { interface Factory {
fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter fun create(oneToOne: Boolean): EditDefaultNotificationSettingPresenter
} }
@Composable @Composable
override fun present(): EditDefaultNotificationSettingState { override fun present(): EditDefaultNotificationSettingState {
var displayMentionsOnlyDisclaimer by remember { mutableStateOf(false) } var displayMentionsOnlyDisclaimer by remember { mutableStateOf(false) }
@ -63,7 +64,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
mutableStateOf(null) mutableStateOf(null)
} }
val changeNotificationSettingAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>> = remember { val roomsWithUserDefinedMode: MutableState<List<RoomSummary.Filled>> = remember {
mutableStateOf(listOf()) mutableStateOf(listOf())
@ -82,7 +83,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> { is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> {
localCoroutineScope.setDefaultNotificationMode(event.mode, changeNotificationSettingAction) localCoroutineScope.setDefaultNotificationMode(event.mode, changeNotificationSettingAction)
} }
EditDefaultNotificationSettingStateEvents.ClearError -> changeNotificationSettingAction.value = Async.Uninitialized EditDefaultNotificationSettingStateEvents.ClearError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized
} }
} }
@ -132,17 +133,16 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
roomWithUserDefinedRules.contains(it.identifier()) && isOneToOne == room.isOneToOne roomWithUserDefinedRules.contains(it.identifier()) && isOneToOne == room.isOneToOne
} }
// locale sensitive sorting // locale sensitive sorting
.sortedWith(compareBy(Collator.getInstance()){ it.details.name }) .sortedWith(compareBy(Collator.getInstance()) { it.details.name })
roomsWithUserDefinedMode.value = sortedSummaries roomsWithUserDefinedMode.value = sortedSummaries
} }
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {
suspend { suspend {
// On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did).
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne).getOrThrow() notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne).getOrThrow()
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne).getOrThrow() notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne).getOrThrow()
}.runCatchingUpdatingState(action) }.runCatchingUpdatingState(action)
} }
} }

View file

@ -16,7 +16,7 @@
package io.element.android.features.preferences.impl.notifications.edit package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -25,7 +25,7 @@ data class EditDefaultNotificationSettingState(
val isOneToOne: Boolean, val isOneToOne: Boolean,
val mode: RoomNotificationMode?, val mode: RoomNotificationMode?,
val roomsWithUserDefinedMode: ImmutableList<RoomSummary.Filled>, val roomsWithUserDefinedMode: ImmutableList<RoomSummary.Filled>,
val changeNotificationSettingAction: Async<Unit>, val changeNotificationSettingAction: AsyncAction<Unit>,
val displayMentionsOnlyDisclaimer: Boolean, val displayMentionsOnlyDisclaimer: Boolean,
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,
) )

View file

@ -17,27 +17,27 @@
package io.element.android.features.preferences.impl.notifications.edit package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
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.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
open class EditDefaultNotificationSettingStateProvider: PreviewParameterProvider<EditDefaultNotificationSettingState> { open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
override val values: Sequence<EditDefaultNotificationSettingState> override val values: Sequence<EditDefaultNotificationSettingState>
get() = sequenceOf( get() = sequenceOf(
anEditDefaultNotificationSettingsState(), anEditDefaultNotificationSettingsState(),
anEditDefaultNotificationSettingsState(isOneToOne = true), anEditDefaultNotificationSettingsState(isOneToOne = true),
anEditDefaultNotificationSettingsState(changeNotificationSettingAction = Async.Loading(Unit)), anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
anEditDefaultNotificationSettingsState(changeNotificationSettingAction = Async.Failure(Throwable("error"))), anEditDefaultNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))),
anEditDefaultNotificationSettingsState(displayMentionsOnlyDisclaimer = true), anEditDefaultNotificationSettingsState(displayMentionsOnlyDisclaimer = true),
) )
} }
private fun anEditDefaultNotificationSettingsState( private fun anEditDefaultNotificationSettingsState(
isOneToOne: Boolean = false, isOneToOne: Boolean = false,
changeNotificationSettingAction: Async<Unit> = Async.Uninitialized, changeNotificationSettingAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
displayMentionsOnlyDisclaimer: Boolean = false, displayMentionsOnlyDisclaimer: Boolean = false,
) = EditDefaultNotificationSettingState( ) = EditDefaultNotificationSettingState(
isOneToOne = isOneToOne, isOneToOne = isOneToOne,

View file

@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -117,7 +117,7 @@ fun EditDefaultNotificationSettingView(
} }
} }
} }
AsyncView( AsyncActionView(
async = state.changeNotificationSettingAction, async = state.changeNotificationSettingAction,
errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) }, errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) },
onErrorDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) }, onErrorDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) },

View file

@ -17,7 +17,7 @@
package io.element.android.features.preferences.impl.root package io.element.android.features.preferences.impl.root
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -39,7 +39,6 @@ fun aPreferencesRootState() = PreferencesRootState(
fun aDirectLogoutState() = DirectLogoutState( fun aDirectLogoutState() = DirectLogoutState(
canDoDirectSignOut = true, canDoDirectSignOut = true,
showConfirmationDialog = false, logoutAction = AsyncAction.Uninitialized,
logoutAction = Async.Uninitialized,
eventSink = {}, eventSink = {},
) )

View file

@ -31,7 +31,7 @@ import androidx.core.net.toUri
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async 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.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
@ -92,7 +92,7 @@ class EditUserProfilePresenter @AssistedInject constructor(
} }
} }
val saveAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
fun handleEvents(event: EditUserProfileEvents) { fun handleEvents(event: EditUserProfileEvents) {
when (event) { when (event) {
@ -111,7 +111,7 @@ class EditUserProfilePresenter @AssistedInject constructor(
} }
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
EditUserProfileEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized EditUserProfileEvents.CancelSaveChanges -> saveAction.value = AsyncAction.Uninitialized
} }
} }
@ -126,7 +126,7 @@ class EditUserProfilePresenter @AssistedInject constructor(
displayName = userDisplayName.orEmpty(), displayName = userDisplayName.orEmpty(),
userAvatarUrl = userAvatarUri, userAvatarUrl = userAvatarUri,
avatarActions = avatarActions, avatarActions = avatarActions,
saveButtonEnabled = canSave && saveAction.value !is Async.Loading, saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading,
saveAction = saveAction.value, saveAction = saveAction.value,
cameraPermissionState = cameraPermissionState, cameraPermissionState = cameraPermissionState,
eventSink = { handleEvents(it) }, eventSink = { handleEvents(it) },
@ -140,7 +140,12 @@ class EditUserProfilePresenter @AssistedInject constructor(
// Need to call `toUri()?.toString()` to make the test pass (we mockk Uri) // Need to call `toUri()?.toString()` to make the test pass (we mockk Uri)
avatarUri?.toString()?.trim() != currentUser.avatarUrl?.toUri()?.toString()?.trim() avatarUri?.toString()?.trim() != currentUser.avatarUrl?.toUri()?.toString()?.trim()
private fun CoroutineScope.saveChanges(name: String?, avatarUri: Uri?, currentUser: MatrixUser, action: MutableState<Async<Unit>>) = launch { private fun CoroutineScope.saveChanges(
name: String?,
avatarUri: Uri?,
currentUser: MatrixUser,
action: MutableState<AsyncAction<Unit>>,
) = launch {
val results = mutableListOf<Result<Unit>>() val results = mutableListOf<Result<Unit>>()
suspend { suspend {
if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) { if (!name.isNullOrEmpty() && name.trim() != currentUser.displayName.orEmpty().trim()) {

View file

@ -17,7 +17,7 @@
package io.element.android.features.preferences.impl.user.editprofile package io.element.android.features.preferences.impl.user.editprofile
import android.net.Uri import android.net.Uri
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
@ -29,7 +29,7 @@ data class EditUserProfileState(
val userAvatarUrl: Uri?, val userAvatarUrl: Uri?,
val avatarActions: ImmutableList<AvatarAction>, val avatarActions: ImmutableList<AvatarAction>,
val saveButtonEnabled: Boolean, val saveButtonEnabled: Boolean,
val saveAction: Async<Unit>, val saveAction: AsyncAction<Unit>,
val cameraPermissionState: PermissionsState, val cameraPermissionState: PermissionsState,
val eventSink: (EditUserProfileEvents) -> Unit val eventSink: (EditUserProfileEvents) -> Unit
) )

View file

@ -17,7 +17,7 @@
package io.element.android.features.preferences.impl.user.editprofile package io.element.android.features.preferences.impl.user.editprofile
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.api.aPermissionsState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -35,7 +35,7 @@ fun aEditUserProfileState() = EditUserProfileState(
displayName = "John Doe", displayName = "John Doe",
userAvatarUrl = null, userAvatarUrl = null,
avatarActions = persistentListOf(), avatarActions = persistentListOf(),
saveAction = Async.Uninitialized, saveAction = AsyncAction.Uninitialized,
saveButtonEnabled = true, saveButtonEnabled = true,
cameraPermissionState = aPermissionsState(showDialog = false), cameraPermissionState = aPermissionsState(showDialog = false),
eventSink = {} eventSink = {}

View file

@ -43,7 +43,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField import io.element.android.libraries.designsystem.components.LabelledOutlinedTextField
import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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
@ -57,6 +57,7 @@ import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.EditableAvatarView import io.element.android.libraries.matrix.ui.components.EditableAvatarView
import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -147,9 +148,13 @@ fun EditUserProfileView(
onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) } onActionSelected = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) }
) )
AsyncView( AsyncActionView(
async = state.saveAction, async = state.saveAction,
progressText = stringResource(R.string.screen_edit_profile_updating_details), progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(R.string.screen_edit_profile_updating_details),
)
},
onSuccess = { onProfileEdited() }, onSuccess = { onProfileEdited() },
errorTitle = { stringResource(R.string.screen_edit_profile_error_title) }, errorTitle = { stringResource(R.string.screen_edit_profile_error_title) },
errorMessage = { stringResource(R.string.screen_edit_profile_error) }, errorMessage = { stringResource(R.string.screen_edit_profile_error) },

View file

@ -26,7 +26,7 @@ import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUs
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
@ -49,8 +49,8 @@ class DeveloperSettingsPresenterTest {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.features).isEmpty() assertThat(initialState.features).isEmpty()
assertThat(initialState.clearCacheAction).isEqualTo(Async.Uninitialized) assertThat(initialState.clearCacheAction).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.cacheSize).isEqualTo(Async.Uninitialized) assertThat(initialState.cacheSize).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.customElementCallBaseUrlState).isNotNull() assertThat(initialState.customElementCallBaseUrlState).isNotNull()
assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull() assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
val loadedState = awaitItem() val loadedState = awaitItem()
@ -105,9 +105,9 @@ class DeveloperSettingsPresenterTest {
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse() assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
initialState.eventSink(DeveloperSettingsEvents.ClearCache) initialState.eventSink(DeveloperSettingsEvents.ClearCache)
val stateAfterEvent = awaitItem() val stateAfterEvent = awaitItem()
assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(Async.Loading::class.java) assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(AsyncData.Loading::class.java)
skipItems(1) skipItems(1)
assertThat(awaitItem().clearCacheAction).isInstanceOf(Async.Success::class.java) assertThat(awaitItem().clearCacheAction).isInstanceOf(AsyncData.Success::class.java)
assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue() assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }

View file

@ -23,7 +23,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
@ -47,8 +47,7 @@ class PreferencesRootPresenterTest {
private val aDirectLogoutState = DirectLogoutState( private val aDirectLogoutState = DirectLogoutState(
canDoDirectSignOut = true, canDoDirectSignOut = true,
showConfirmationDialog = false, logoutAction = AsyncAction.Uninitialized,
logoutAction = Async.Uninitialized,
eventSink = {}, eventSink = {},
) )

View file

@ -21,7 +21,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@ -110,7 +110,7 @@ class EditUserProfilePresenterTest {
AvatarAction.Remove AvatarAction.Remove
) )
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
assertThat(initialState.saveAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(initialState.saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
@ -359,7 +359,7 @@ class EditUserProfilePresenterTest {
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2) skipItems(2)
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
} }
} }
@ -406,9 +406,9 @@ class EditUserProfilePresenterTest {
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo")) initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo"))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(2) skipItems(2)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
initialState.eventSink(EditUserProfileEvents.CancelSaveChanges) initialState.eventSink(EditUserProfileEvents.CancelSaveChanges)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
@ -421,8 +421,8 @@ class EditUserProfilePresenterTest {
initialState.eventSink(event) initialState.eventSink(event)
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvents.Save)
skipItems(1) skipItems(1)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
} }
} }

View file

@ -30,7 +30,7 @@ import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.impl.logs.VectorFileLogger import io.element.android.features.rageshake.impl.logs.VectorFileLogger
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,27 +45,27 @@ class BugReportPresenter @Inject constructor(
private class BugReporterUploadListener( private class BugReporterUploadListener(
private val sendingProgress: MutableFloatState, private val sendingProgress: MutableFloatState,
private val sendingAction: MutableState<Async<Unit>> private val sendingAction: MutableState<AsyncAction<Unit>>
) : BugReporterListener { ) : BugReporterListener {
override fun onUploadCancelled() { override fun onUploadCancelled() {
sendingProgress.floatValue = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Uninitialized sendingAction.value = AsyncAction.Uninitialized
} }
override fun onUploadFailed(reason: String?) { override fun onUploadFailed(reason: String?) {
sendingProgress.floatValue = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Failure(Exception(reason)) sendingAction.value = AsyncAction.Failure(Exception(reason))
} }
override fun onProgress(progress: Int) { override fun onProgress(progress: Int) {
sendingProgress.floatValue = progress.toFloat() / 100 sendingProgress.floatValue = progress.toFloat() / 100
sendingAction.value = Async.Loading() sendingAction.value = AsyncAction.Loading
} }
override fun onUploadSucceed() { override fun onUploadSucceed() {
sendingProgress.floatValue = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Success(Unit) sendingAction.value = AsyncAction.Success(Unit)
} }
} }
@ -83,8 +83,8 @@ class BugReportPresenter @Inject constructor(
val sendingProgress = remember { val sendingProgress = remember {
mutableFloatStateOf(0f) mutableFloatStateOf(0f)
} }
val sendingAction: MutableState<Async<Unit>> = remember { val sendingAction: MutableState<AsyncAction<Unit>> = remember {
mutableStateOf(Async.Uninitialized) mutableStateOf(AsyncAction.Uninitialized)
} }
val formState: MutableState<BugReportFormState> = remember { val formState: MutableState<BugReportFormState> = remember {
mutableStateOf(BugReportFormState.Default) mutableStateOf(BugReportFormState.Default)
@ -109,7 +109,7 @@ class BugReportPresenter @Inject constructor(
} }
BugReportEvents.ClearError -> { BugReportEvents.ClearError -> {
sendingProgress.floatValue = 0f sendingProgress.floatValue = 0f
sendingAction.value = Async.Uninitialized sendingAction.value = AsyncAction.Uninitialized
} }
} }
} }

View file

@ -17,7 +17,7 @@
package io.element.android.features.rageshake.impl.bugreport package io.element.android.features.rageshake.impl.bugreport
import android.os.Parcelable import android.os.Parcelable
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
data class BugReportState( data class BugReportState(
@ -25,11 +25,11 @@ data class BugReportState(
val hasCrashLogs: Boolean, val hasCrashLogs: Boolean,
val screenshotUri: String?, val screenshotUri: String?,
val sendingProgress: Float, val sendingProgress: Float,
val sending: Async<Unit>, val sending: AsyncAction<Unit>,
val eventSink: (BugReportEvents) -> Unit val eventSink: (BugReportEvents) -> Unit
) { ) {
val submitEnabled = val submitEnabled =
formState.description.length > 10 && sending !is Async.Loading formState.description.length > 10 && sending !is AsyncAction.Loading
} }
@Parcelize @Parcelize

Some files were not shown because too many files have changed in this diff Show more