Merge branch 'develop' into feature/fga/invite_user_loader

This commit is contained in:
ganfra
2024-01-05 14:02:58 +01:00
496 changed files with 1390 additions and 1180 deletions

View File

@@ -11,9 +11,10 @@ jobs:
welcome:
runs-on: ubuntu-latest
name: Welcome comment
if: github.event.pull_request.fork != null
steps:
- name: Add auto-generated commit warning
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
@@ -22,9 +23,9 @@ jobs:
repo: context.repo.repo,
body: `Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:
- Your branch should be based on `origin/develop`, at least when it was created.
- There is a changelog entry in the `changelog.d` folder with [the Towncrier format](https://towncrier.readthedocs.io/en/latest/tutorial.html#creating-news-fragments).
- The test pass locally running `./gradlew test`.
- The code quality check suite pass locally running `./gradlew runQualityChecks`.
- If you modified anything related to the UI, including previews, you'll have to run the `Record screenshots` GH action in your forked repo: that will generate compatible new screenshots. However, given Github Actions limitations, **it will prevent the CI from running temporarily**, until you upload a new commit after that one. To do so, just pull the latest changes and push [an empty commit](https://coderwall.com/p/vkdekq/git-commit-allow-empty).`
- Your branch should be based on \`origin/develop\`, at least when it was created.
- There is a changelog entry in the \`changelog.d\` folder with [the Towncrier format](https://towncrier.readthedocs.io/en/latest/tutorial.html#creating-news-fragments).
- The test pass locally running \`./gradlew test\`.
- The code quality check suite pass locally running \`./gradlew runQualityChecks\`.
- If you modified anything related to the UI, including previews, you'll have to run the \`Record screenshots\` GH action in your forked repo: that will generate compatible new screenshots. However, given Github Actions limitations, **it will prevent the CI from running temporarily**, until you upload a new commit after that one. To do so, just pull the latest changes and push [an empty commit](https://coderwall.com/p/vkdekq/git-commit-allow-empty).`
})

View File

@@ -24,31 +24,31 @@ jobs:
cancel-in-progress: true
steps:
- name: Remove Run-Maestro label
if: ${{ !github.event.pull_request.fork && github.event.label.name == 'Run-Maestro' }}
if: ${{ github.event_name == 'pull_request' && github.event.label.name == 'Run-Maestro' }}
uses: actions-ecosystem/action-remove-labels@v1
with:
labels: Run-Maestro
- uses: actions/checkout@v4
if: ${{ !github.event.pull_request.fork }}
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- uses: actions/setup-java@v4
name: Use JDK 17
if: ${{ !github.event.pull_request.fork }}
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Assemble debug APK
run: ./gradlew :app:assembleDebug $CI_GRADLE_ARG_PROPERTIES
if: ${{ !github.event.pull_request.fork }}
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.0
if: ${{ !github.event.pull_request.fork }}
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):

View File

@@ -10,6 +10,9 @@
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }
# TagSoup, coming from the RTE library
-keep class org.ccil.cowan.tagsoup.** { *; }
# kotlinx.serialization
# Kotlin serialization looks up the generated serializer classes through a function on companion

1
changelog.d/1949.bugfix Normal file
View File

@@ -0,0 +1 @@
Make sure the media viewer tries the main url first (if not empty) then the thumbnail url and then not open if both are missing instead of failing with an error dialog

1
changelog.d/2099.bugfix Normal file
View File

@@ -0,0 +1 @@
Trim whitespace at the end of messages to ensure we render the right content.

1
changelog.d/2105.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix crashes in room list when the last message for a room was an extremely long one (several thousands of characters) with no line breaks.

1
changelog.d/2127.misc Normal file
View File

@@ -0,0 +1 @@
Remove extra previews for timestamp view with 'document' case

1
changelog.d/2142.misc Normal file
View File

@@ -0,0 +1 @@
Bump AGP version to 8.2.0

1
changelog.d/2155.bugfix Normal file
View File

@@ -0,0 +1 @@
Use `SubomposeLayout` for `ContentAvoidingLayout` to prevent wrong measurements in the layout process, leading to cut-off text messages in the timeline.

1
changelog.d/2159.feature Normal file
View File

@@ -0,0 +1 @@
Added support for MSC4027 (render custom images in reactions)

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

View File

@@ -16,10 +16,10 @@
package io.element.android.features.call.ui
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncData
data class CallScreenState(
val urlState: Async<String>,
val urlState: AsyncData<String>,
val userAgent: String,
val isInWidgetMode: Boolean,
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.features.call.R
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.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -96,7 +96,7 @@ internal fun CallScreenView(
@Composable
private fun CallWebView(
url: Async<String>,
url: AsyncData<String>,
userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit,
onWebViewCreated: (WebView) -> Unit,
@@ -116,7 +116,7 @@ private fun CallWebView(
}
},
update = { webView ->
if (url is Async.Success && webView.url != url.data) {
if (url is AsyncData.Success && webView.url != url.data) {
webView.loadUrl(url.data)
}
},
@@ -161,7 +161,7 @@ internal fun CallScreenViewPreview() {
ElementPreview {
CallScreenView(
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,
userAgent = "",
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.utils.FakeCallWidgetProvider
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.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -62,7 +62,7 @@ class CallScreenPresenterTest {
skipItems(1)
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()
}
}
@@ -83,7 +83,7 @@ class CallScreenPresenterTest {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java)
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1)

View File

@@ -17,7 +17,7 @@
package io.element.android.features.createroom.api
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.UserId
@@ -27,5 +27,5 @@ interface StartDMAction {
* @param userId The user to start a DM with.
* @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 im.vector.app.features.analytics.plan.CreatedRoom
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.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@@ -36,17 +36,17 @@ class DefaultStartDMAction @Inject constructor(
private val analyticsService: AnalyticsService,
) : StartDMAction {
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) {
actionState.value = Async.Loading()
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
actionState.value = AsyncAction.Loading
when (val result = matrixClient.startDM(userId)) {
is StartDMResult.Success -> {
if (result.isNew) {
analyticsService.capture(CreatedRoom(isDM = true))
}
actionState.value = Async.Success(result.roomId)
actionState.value = AsyncAction.Success(result.roomId)
}
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 io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes
@@ -91,10 +91,10 @@ class ConfigureRoomPresenter @Inject constructor(
}
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) {
createRoomAction.value = Async.Uninitialized
createRoomAction.value = AsyncAction.Uninitialized
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(
config: CreateRoomConfig,
createRoomAction: MutableState<Async<RoomId>>
createRoomAction: MutableState<AsyncAction<RoomId>>
) = launch {
suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }

View File

@@ -16,17 +16,17 @@
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.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.ui.media.AvatarAction
import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList
data class ConfigureRoomState(
val config: CreateRoomConfig,
val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: Async<RoomId>,
val createRoomAction: AsyncAction<RoomId>,
val cameraPermissionState: PermissionsState,
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 io.element.android.features.createroom.impl.CreateRoomConfig
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 kotlinx.collections.immutable.persistentListOf
@@ -41,7 +41,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
fun aConfigureRoomState() = ConfigureRoomState(
config = CreateRoomConfig(),
avatarActions = persistentListOf(),
createRoomAction = Async.Uninitialized,
createRoomAction = AsyncAction.Uninitialized,
cameraPermissionState = aPermissionsState(showDialog = false),
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.components.RoomPrivacyOption
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.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -148,9 +149,13 @@ fun ConfigureRoomView(
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
)
AsyncView(
AsyncActionView(
async = state.createRoomAction,
progressText = stringResource(CommonStrings.common_creating_room),
progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_creating_room),
)
},
onSuccess = { onRoomCreated(it) },
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
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.UserListPresenter
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.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.core.RoomId
@@ -55,14 +55,14 @@ class CreateRoomRootPresenter @Inject constructor(
val userListState = presenter.present()
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) {
when (event) {
is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
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
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
data class CreateRoomRootState(
val applicationName: String,
val userListState: UserListState,
val startDmAction: Async<RoomId>,
val startDmAction: AsyncAction<RoomId>,
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 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.matrix.ui.components.aMatrixUser
import io.element.android.libraries.usersearch.api.UserSearchResult
@@ -30,7 +30,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
get() = sequenceOf(
aCreateRoomRootState(),
aCreateRoomRootState().copy(
startDmAction = Async.Loading(),
startDmAction = AsyncAction.Loading,
userListState = aMatrixUser().let {
aUserListState().copy(
searchQuery = it.userId.value,
@@ -41,7 +41,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
}
),
aCreateRoomRootState().copy(
startDmAction = Async.Failure(Throwable("error")),
startDmAction = AsyncAction.Failure(Throwable("error")),
userListState = aMatrixUser().let {
aUserListState().copy(
searchQuery = it.userId.value,
@@ -57,6 +57,6 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
fun aCreateRoomRootState() = CreateRoomRootState(
eventSink = {},
applicationName = "Element X Preview",
startDmAction = Async.Uninitialized,
startDmAction = AsyncAction.Uninitialized,
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.features.createroom.impl.R
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.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -93,9 +94,13 @@ fun CreateRoomRootView(
}
}
AsyncView(
AsyncActionView(
async = state.startDmAction,
progressText = stringResource(CommonStrings.common_starting_chat),
progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_starting_chat),
)
},
onSuccess = { onOpenDM(it) },
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
onRetry = {

View File

@@ -19,7 +19,7 @@ package io.element.android.features.createroom.impl
import androidx.compose.runtime.mutableStateOf
import com.google.common.truth.Truth.assertThat
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.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -39,9 +39,9 @@ class DefaultStartDMActionTests {
givenFindDmResult(A_ROOM_ID)
}
val action = createStartDMAction(matrixClient)
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized)
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
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
@@ -52,9 +52,9 @@ class DefaultStartDMActionTests {
}
val analyticsService = FakeAnalyticsService()
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)
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))
}
@@ -65,9 +65,9 @@ class DefaultStartDMActionTests {
givenCreateDmResult(Result.failure(A_THROWABLE))
}
val action = createStartDMAction(matrixClient)
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized)
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
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(

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.CreateRoomDataStore
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.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_MESSAGE
@@ -234,9 +234,9 @@ class ConfigureRoomPresenterTests {
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
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()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Success::class.java)
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
}
}
@@ -272,16 +272,16 @@ class ConfigureRoomPresenterTests {
val initialState = awaitItem()
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()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java)
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Success::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
}
}
@@ -297,22 +297,22 @@ class ConfigureRoomPresenterTests {
// Create
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()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterRetry = awaitItem()
assertThat(stateAfterRetry.createRoomAction).isInstanceOf(Async.Failure::class.java)
assertThat((stateAfterRetry.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Cancel
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.UserListDataStore
import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -52,20 +51,20 @@ class CreateRoomRootPresenterTests {
}.test {
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.userListState.selectedUsers).isEmpty()
assertThat(initialState.userListState.isSearchActive).isFalse()
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
val matrixUser = MatrixUser(UserId("@name:domain"))
val startDMSuccessResult = Async.Success(A_ROOM_ID)
val startDMFailureResult = Async.Failure<RoomId>(A_THROWABLE)
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
// Failure
startDMAction.givenExecuteResult(startDMFailureResult)
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(startDMFailureResult)
state.eventSink(CreateRoomRootEvents.CancelStartDM)
@@ -74,10 +73,10 @@ class CreateRoomRootPresenterTests {
// Success
startDMAction.givenExecuteResult(startDMSuccessResult)
awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(Async.Uninitialized)
assertThat(state.startDmAction).isEqualTo(AsyncAction.Uninitialized)
state.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
}
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state ->
assertThat(state.startDmAction).isEqualTo(startDMSuccessResult)
}

View File

@@ -18,7 +18,7 @@ package io.element.android.features.createroom.test
import androidx.compose.runtime.MutableState
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.UserId
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -26,14 +26,14 @@ import kotlinx.coroutines.delay
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
}
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) {
actionState.value = Async.Loading()
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
actionState.value = AsyncAction.Loading
delay(1)
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.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -77,14 +77,14 @@ class InviteListPresenter @Inject constructor(
}
val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
val acceptedAction: MutableState<AsyncData<RoomId>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val declinedAction: MutableState<AsyncData<Unit>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val decliningInvite: MutableState<InviteListInviteSummary?> = remember { mutableStateOf(null) }
fun handleEvent(event: InviteListEvents) {
when (event) {
is InviteListEvents.AcceptInvite -> {
acceptedAction.value = Async.Uninitialized
acceptedAction.value = AsyncData.Uninitialized
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
@@ -93,7 +93,7 @@ class InviteListPresenter @Inject constructor(
}
is InviteListEvents.ConfirmDeclineInvite -> {
declinedAction.value = Async.Uninitialized
declinedAction.value = AsyncData.Uninitialized
decliningInvite.value?.let {
localCoroutineScope.declineInvite(it.roomId, declinedAction)
}
@@ -105,11 +105,11 @@ class InviteListPresenter @Inject constructor(
}
is InviteListEvents.DismissAcceptError -> {
acceptedAction.value = Async.Uninitialized
acceptedAction.value = AsyncData.Uninitialized
}
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 {
client.getRoom(roomId)?.use {
it.join().getOrThrow()
@@ -148,7 +148,7 @@ class InviteListPresenter @Inject constructor(
}.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 {
client.getRoom(roomId)?.use {
it.leave().getOrThrow()

View File

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

View File

@@ -35,7 +35,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
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.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@@ -56,7 +56,7 @@ fun InviteListView(
onInviteAccepted: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
if (state.acceptedAction is Async.Success) {
if (state.acceptedAction is AsyncData.Success) {
LaunchedEffect(state.acceptedAction) {
onInviteAccepted(state.acceptedAction.data)
}
@@ -89,7 +89,7 @@ fun InviteListView(
)
}
if (state.acceptedAction is Async.Failure) {
if (state.acceptedAction is AsyncData.Failure) {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
title = stringResource(CommonStrings.common_error),
@@ -98,7 +98,7 @@ fun InviteListView(
)
}
if (state.declinedAction is Async.Failure) {
if (state.declinedAction is AsyncData.Failure) {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
title = stringResource(CommonStrings.common_error),

View File

@@ -22,7 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invitelist.api.SeenInvitesStore
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.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
@@ -245,7 +245,7 @@ class InviteListPresenterTests {
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()
assertThat(newState.declinedAction).isEqualTo(Async.Uninitialized)
assertThat(newState.declinedAction).isEqualTo(AsyncData.Uninitialized)
}
}
@@ -300,7 +300,7 @@ class InviteListPresenterTests {
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)
}
}
@@ -323,7 +323,7 @@ class InviteListPresenterTests {
val originalState = awaitItem()
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)
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.model.PinEntry
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.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
@@ -49,11 +49,11 @@ class PinUnlockPresenter @Inject constructor(
@Composable
override fun present(): PinUnlockState {
val pinEntryState = remember {
mutableStateOf<Async<PinEntry>>(Async.Uninitialized)
mutableStateOf<AsyncData<PinEntry>>(AsyncData.Uninitialized)
}
val pinEntry by pinEntryState
var remainingAttempts by remember {
mutableStateOf<Async<Int>>(Async.Uninitialized)
mutableStateOf<AsyncData<Int>>(AsyncData.Uninitialized)
}
var showWrongPinTitle by rememberSaveable {
mutableStateOf(false)
@@ -62,7 +62,7 @@ class PinUnlockPresenter @Inject constructor(
mutableStateOf(false)
}
val signOutAction = remember {
mutableStateOf<Async<String?>>(Async.Uninitialized)
mutableStateOf<AsyncData<String?>>(AsyncData.Uninitialized)
}
var biometricUnlockResult by remember {
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null)
@@ -91,7 +91,7 @@ class PinUnlockPresenter @Inject constructor(
}
}
val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber()
remainingAttempts = Async.Success(remainingAttemptsNumber)
remainingAttempts = AsyncData.Success(remainingAttemptsNumber)
if (remainingAttemptsNumber == 0) {
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()
}
private fun Async<PinEntry>.toText(): String {
private fun AsyncData<PinEntry>.toText(): String {
return dataOrNull()?.toText() ?: ""
}
private fun Async<PinEntry>.clear(): Async<PinEntry> {
private fun AsyncData<PinEntry>.clear(): AsyncData<PinEntry> {
return when (this) {
is Async.Success -> Async.Success(data.clear())
is AsyncData.Success -> AsyncData.Success(data.clear())
else -> this
}
}
private fun Async<PinEntry>.process(pinKeypadModel: PinKeypadModel): Async<PinEntry> {
private fun AsyncData<PinEntry>.process(pinKeypadModel: PinKeypadModel): AsyncData<PinEntry> {
return when (this) {
is Async.Success -> {
is AsyncData.Success -> {
val pinEntry = when (pinKeypadModel) {
PinKeypadModel.Back -> data.deleteLast()
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
PinKeypadModel.Empty -> data
}
Async.Success(pinEntry)
AsyncData.Success(pinEntry)
}
else -> this
}
}
private fun Async<PinEntry>.process(pinEntryAsText: String): Async<PinEntry> {
private fun AsyncData<PinEntry>.process(pinEntryAsText: String): AsyncData<PinEntry> {
return when (this) {
is Async.Success -> {
is AsyncData.Success -> {
val pinEntry = data.fillWith(pinEntryAsText)
Async.Success(pinEntry)
AsyncData.Success(pinEntry)
}
else -> this
}
}
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
suspend {
matrixClient.logout(ignoreSdkError = true)
}.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.BiometricUnlockError
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(
val pinEntry: Async<PinEntry>,
val pinEntry: AsyncData<PinEntry>,
val showWrongPinTitle: Boolean,
val remainingAttempts: Async<Int>,
val remainingAttempts: AsyncData<Int>,
val showSignOutPrompt: Boolean,
val signOutAction: Async<String?>,
val signOutAction: AsyncData<String?>,
val showBiometricUnlock: Boolean,
val isUnlocked: Boolean,
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
val eventSink: (PinUnlockEvents) -> Unit
) {
val isSignOutPromptCancellable = when (remainingAttempts) {
is Async.Success -> remainingAttempts.data > 0
is AsyncData.Success -> remainingAttempts.data > 0
else -> true
}

View File

@@ -19,7 +19,7 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
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> {
override val values: Sequence<PinUnlockState>
@@ -30,7 +30,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
aPinUnlockState(showSignOutPrompt = true),
aPinUnlockState(showBiometricUnlock = false),
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
aPinUnlockState(signOutAction = Async.Loading()),
aPinUnlockState(signOutAction = AsyncData.Loading()),
)
}
@@ -42,11 +42,11 @@ fun aPinUnlockState(
showBiometricUnlock: Boolean = true,
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
isUnlocked: Boolean = false,
signOutAction: Async<String?> = Async.Uninitialized,
signOutAction: AsyncData<String?> = AsyncData.Uninitialized,
) = PinUnlockState(
pinEntry = Async.Success(pinEntry),
pinEntry = AsyncData.Success(pinEntry),
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = Async.Success(remainingAttempts),
remainingAttempts = AsyncData.Success(remainingAttempts),
showSignOutPrompt = showSignOutPrompt,
showBiometricUnlock = showBiometricUnlock,
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.PinEntry
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.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@@ -92,7 +92,7 @@ fun PinUnlockView(
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))
}
if (state.showBiometricUnlockError) {
@@ -335,7 +335,7 @@ private fun PinUnlockHeader(
style = ElementTheme.typography.fontBodyMdRegular,
color = subtitleColor,
)
if (!isInAppUnlock && state.pinEntry is Async.Success) {
if (!isInAppUnlock && state.pinEntry is AsyncData.Success) {
Spacer(Modifier.height(24.dp))
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.assertText
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.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.consumeItemsUntilPredicate
@@ -48,15 +48,15 @@ class PinUnlockPresenterTest {
presenter.present()
}.test {
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.showSignOutPrompt).isFalse()
assertThat(state.isUnlocked).isFalse()
assertThat(state.signOutAction).isInstanceOf(Async.Uninitialized::class.java)
assertThat(state.remainingAttempts).isInstanceOf(Async.Uninitialized::class.java)
assertThat(state.signOutAction).isInstanceOf(AsyncData.Uninitialized::class.java)
assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Uninitialized::class.java)
}
consumeItemsUntilPredicate {
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success
it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
}.last().also { state ->
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1')))
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2')))
@@ -83,7 +83,7 @@ class PinUnlockPresenterTest {
presenter.present()
}.test {
val initialState = consumeItemsUntilPredicate {
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success
it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
}.last()
val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0
repeat(numberOfAttempts) {
@@ -107,7 +107,7 @@ class PinUnlockPresenterTest {
presenter.present()
}.test {
consumeItemsUntilPredicate {
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success
it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
}.last().also { state ->
state.eventSink(PinUnlockEvents.OnForgetPin)
}
@@ -125,12 +125,12 @@ class PinUnlockPresenterTest {
state.eventSink(PinUnlockEvents.SignOut)
}
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)
}

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.AccountProviderDataSource
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.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -41,14 +41,14 @@ class ChangeServerPresenter @Inject constructor(
override fun present(): ChangeServerState {
val localCoroutineScope = rememberCoroutineScope()
val changeServerAction: MutableState<Async<Unit>> = remember {
mutableStateOf(Async.Uninitialized)
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
fun handleEvents(event: ChangeServerEvents) {
when (event) {
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(
data: AccountProvider,
changeServerAction: MutableState<Async<Unit>>,
changeServerAction: MutableState<AsyncData<Unit>>,
) = launch {
suspend {
authenticationService.setHomeserver(data.url).map {

View File

@@ -16,9 +16,9 @@
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(
val changeServerAction: Async<Unit>,
val changeServerAction: AsyncData<Unit>,
val eventSink: (ChangeServerEvents) -> Unit
)

View File

@@ -17,7 +17,7 @@
package io.element.android.features.login.impl.changeserver
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> {
override val values: Sequence<ChangeServerState>
@@ -27,6 +27,6 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
}
fun aChangeServerState() = ChangeServerState(
changeServerAction = Async.Uninitialized,
changeServerAction = AsyncData.Uninitialized,
eventSink = {}
)

View File

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

View File

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

View File

@@ -17,20 +17,20 @@
package io.element.android.features.login.impl.oidc.webview
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
open class OidcStateProvider : PreviewParameterProvider<OidcState> {
override val values: Sequence<OidcState>
get() = sequenceOf(
aOidcState(),
aOidcState().copy(requestState = Async.Loading()),
aOidcState().copy(requestState = AsyncAction.Loading),
)
}
fun aOidcState() = OidcState(
oidcDetails = aOidcDetails(),
requestState = Async.Uninitialized,
requestState = AsyncAction.Uninitialized,
eventSink = {}
)

View File

@@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.features.login.impl.oidc.OidcUrlParser
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.PreviewsDayNight
@@ -76,7 +76,7 @@ fun OidcView(
}
)
AsyncView(
AsyncActionView(
async = state.requestState,
onSuccess = { onNavigateBack() },
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.error.ChangeServerError
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.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -61,8 +61,8 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
val accountProvider by accountProviderDataSource.flow().collectAsState()
val localCoroutineScope = rememberCoroutineScope()
val loginFlowAction: MutableState<Async<LoginFlow>> = remember {
mutableStateOf(Async.Uninitialized)
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
LaunchedEffect(Unit) {
@@ -78,7 +78,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
ConfirmAccountProviderEvents.Continue -> {
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(
homeserverUrl: String,
loginFlowAction: MutableState<Async<LoginFlow>>,
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
) = launch {
suspend {
authenticationService.setHomeserver(homeserverUrl).map {
@@ -111,17 +111,17 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
private suspend fun onOidcAction(
oidcAction: OidcAction,
loginFlowAction: MutableState<Async<LoginFlow>>,
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
) {
loginFlowAction.value = Async.Loading()
loginFlowAction.value = AsyncData.Loading()
when (oidcAction) {
OidcAction.GoBack -> {
authenticationService.cancelOidcLogin()
.onSuccess {
loginFlowAction.value = Async.Uninitialized
loginFlowAction.value = AsyncData.Uninitialized
}
.onFailure { failure ->
loginFlowAction.value = Async.Failure(failure)
loginFlowAction.value = AsyncData.Failure(failure)
}
}
is OidcAction.Success -> {
@@ -130,7 +130,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
defaultLoginUserStory.setLoginFlowIsDone(true)
}
.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
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
// Do not use default value, so no member get forgotten in the presenters.
data class ConfirmAccountProviderState(
val accountProvider: AccountProvider,
val isAccountCreation: Boolean,
val loginFlow: Async<LoginFlow>,
val loginFlow: AsyncData<LoginFlow>,
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 {

View File

@@ -18,7 +18,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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> {
override val values: Sequence<ConfirmAccountProviderState>
@@ -31,6 +31,6 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
fun aConfirmAccountProviderState() = ConfirmAccountProviderState(
accountProvider = anAccountProvider(),
isAccountCreation = false,
loginFlow = Async.Uninitialized,
loginFlow = AsyncData.Uninitialized,
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.dialogs.SlidingSyncNotSupportedDialog
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.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -56,7 +56,7 @@ fun ConfirmAccountProviderView(
) {
val isLoading by remember(state.loginFlow) {
derivedStateOf {
state.loginFlow is Async.Loading
state.loginFlow is AsyncData.Loading
}
}
val eventSink = state.eventSink
@@ -107,7 +107,7 @@ fun ConfirmAccountProviderView(
}
) {
when (state.loginFlow) {
is Async.Failure -> {
is AsyncData.Failure -> {
when (val error = state.loginFlow.error) {
is ChangeServerError.Error -> {
ErrorDialog(
@@ -127,14 +127,14 @@ fun ConfirmAccountProviderView(
}
}
}
is Async.Loading -> Unit // The Continue button shows the loading state
is Async.Success -> {
is AsyncData.Loading -> Unit // The Continue button shows the loading state
is AsyncData.Success -> {
when (val loginFlowState = state.loginFlow.data) {
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
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 io.element.android.features.login.impl.DefaultLoginUserStory
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.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
@@ -43,8 +43,8 @@ class LoginPasswordPresenter @Inject constructor(
@Composable
override fun present(): LoginPasswordState {
val localCoroutineScope = rememberCoroutineScope()
val loginAction: MutableState<Async<SessionId>> = remember {
mutableStateOf(Async.Uninitialized)
val loginAction: MutableState<AsyncData<SessionId>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
val formState = rememberSaveable {
@@ -63,7 +63,7 @@ class LoginPasswordPresenter @Inject constructor(
LoginPasswordEvents.Submit -> {
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 {
loggedInState.value = Async.Loading()
private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
loggedInState.value = AsyncData.Loading()
authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId ->
// We will not navigate to the WaitList screen, so the login user story is done
defaultLoginUserStory.setLoginFlowIsDone(true)
loggedInState.value = Async.Success(sessionId)
loggedInState.value = AsyncData.Success(sessionId)
}
.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 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 kotlinx.parcelize.Parcelize
data class LoginPasswordState(
val accountProvider: AccountProvider,
val formState: LoginFormState,
val loginAction: Async<SessionId>,
val loginAction: AsyncData<SessionId>,
val eventSink: (LoginPasswordEvents) -> Unit
) {
val submitEnabled: Boolean
get() = loginAction !is Async.Failure &&
get() = loginAction !is AsyncData.Failure &&
formState.login.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 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> {
override val values: Sequence<LoginPasswordState>
get() = sequenceOf(
aLoginPasswordState(),
// Loading
aLoginPasswordState().copy(loginAction = Async.Loading()),
aLoginPasswordState().copy(loginAction = AsyncData.Loading()),
// Error
aLoginPasswordState().copy(loginAction = Async.Failure(Exception("An error occurred"))),
aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))),
)
}
fun aLoginPasswordState() = LoginPasswordState(
accountProvider = anAccountProvider(),
formState = LoginFormState.Default,
loginAction = Async.Uninitialized,
loginAction = AsyncData.Uninitialized,
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.error.isWaitListError
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.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@@ -84,7 +84,7 @@ fun LoginPasswordView(
) {
val isLoading by remember(state.loginAction) {
derivedStateOf {
state.loginAction is Async.Loading
state.loginAction is AsyncData.Loading
}
}
val focusManager = LocalFocusManager.current
@@ -148,7 +148,7 @@ fun LoginPasswordView(
)
Spacer(modifier = Modifier.height(60.dp))
if (state.loginAction is Async.Failure) {
if (state.loginAction is AsyncData.Failure) {
when {
state.loginAction.error.isWaitListError() -> {
onWaitListError(state.formState)
@@ -224,7 +224,7 @@ private fun LoginForm(
)
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
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.resolver.HomeserverData
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -46,8 +46,8 @@ class SearchAccountProviderPresenter @Inject constructor(
}
val changeServerState = changeServerPresenter.present()
val data: MutableState<Async<List<HomeserverData>>> = remember {
mutableStateOf(Async.Uninitialized)
val data: MutableState<AsyncData<List<HomeserverData>>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
LaunchedEffect(userInput) {
@@ -70,16 +70,16 @@ class SearchAccountProviderPresenter @Inject constructor(
)
}
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<Async<List<HomeserverData>>>) = launch {
data.value = Async.Uninitialized
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<AsyncData<List<HomeserverData>>>) = launch {
data.value = AsyncData.Uninitialized
// Debounce
delay(300)
data.value = Async.Loading()
data.value = AsyncData.Loading()
homeserverResolver.resolve(userInput).collect {
data.value = Async.Success(it)
data.value = AsyncData.Success(it)
}
if (data.value !is Async.Success) {
data.value = Async.Uninitialized
if (data.value !is AsyncData.Success) {
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.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.
data class SearchAccountProviderState(
val userInput: String,
val userInputResult: Async<List<HomeserverData>>,
val userInputResult: AsyncData<List<HomeserverData>>,
val changeServerState: ChangeServerState,
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.features.login.impl.changeserver.aChangeServerState
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> {
override val values: Sequence<SearchAccountProviderState>
get() = sequenceOf(
aSearchAccountProviderState(),
aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())),
aSearchAccountProviderState(userInputResult = AsyncData.Success(aHomeserverDataList())),
// Add other state here
)
}
fun aSearchAccountProviderState(
userInput: String = "",
userInputResult: Async<List<HomeserverData>> = Async.Uninitialized,
userInputResult: AsyncData<List<HomeserverData>> = AsyncData.Uninitialized,
) = SearchAccountProviderState(
userInput = userInput,
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.ChangeServerView
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.components.button.BackButton
import io.element.android.libraries.designsystem.components.form.textFieldState
@@ -152,10 +152,10 @@ fun SearchAccountProviderView(
}
when (state.userInputResult) {
is Async.Failure -> {
is AsyncData.Failure -> {
// Ignore errors (let the user type more chars)
}
is Async.Loading -> {
is AsyncData.Loading -> {
item {
Box(
modifier = Modifier
@@ -167,7 +167,7 @@ fun SearchAccountProviderView(
}
}
}
is Async.Success -> {
is AsyncData.Success -> {
items(state.userInputResult.data) { homeserverData ->
val item = homeserverData.toAccountProvider()
AccountProviderView(
@@ -178,7 +178,7 @@ fun SearchAccountProviderView(
)
}
}
Async.Uninitialized -> Unit
AsyncData.Uninitialized -> Unit
}
item {
Spacer(Modifier.height(32.dp))

View File

@@ -27,7 +27,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.login.impl.DefaultLoginUserStory
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.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -55,8 +55,8 @@ class WaitListPresenter @AssistedInject constructor(
authenticationService.getHomeserverDetails().value?.url ?: "server"
}
val loginAction: MutableState<Async<SessionId>> = remember {
mutableStateOf(Async.Uninitialized)
val loginAction: MutableState<AsyncData<SessionId>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
val attemptNumber = remember { mutableIntStateOf(0) }
@@ -70,7 +70,7 @@ class WaitListPresenter @AssistedInject constructor(
coroutineScope.loginAttempt(formState, loginAction)
}
}
WaitListEvents.ClearError -> loginAction.value = Async.Uninitialized
WaitListEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
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...")
loggedInState.value = Async.Loading()
loggedInState.value = AsyncData.Loading()
authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId ->
loggedInState.value = Async.Success(sessionId)
loggedInState.value = AsyncData.Success(sessionId)
}
.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
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
// Do not use default value, so no member get forgotten in the presenters.
data class WaitListState(
val appName: String,
val serverName: String,
val loginAction: Async<SessionId>,
val loginAction: AsyncData<SessionId>,
val eventSink: (WaitListEvents) -> Unit
)

View File

@@ -17,17 +17,17 @@
package io.element.android.features.login.impl.screens.waitlistscreen
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
open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
override val values: Sequence<WaitListState>
get() = sequenceOf(
aWaitListState(loginAction = Async.Uninitialized),
aWaitListState(loginAction = Async.Loading()),
aWaitListState(loginAction = Async.Failure(Throwable("error"))),
aWaitListState(loginAction = Async.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
aWaitListState(loginAction = Async.Success(SessionId("@alice:element.io"))),
aWaitListState(loginAction = AsyncData.Uninitialized),
aWaitListState(loginAction = AsyncData.Loading()),
aWaitListState(loginAction = AsyncData.Failure(Throwable("error"))),
aWaitListState(loginAction = AsyncData.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
aWaitListState(loginAction = AsyncData.Success(SessionId("@alice:element.io"))),
// Add other state here
)
}
@@ -35,7 +35,7 @@ open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
fun aWaitListState(
appName: String = "Element X",
serverName: String = "server.org",
loginAction: Async<SessionId> = Async.Uninitialized,
loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
) = WaitListState(
appName = appName,
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.error.isWaitListError
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.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -89,12 +89,12 @@ private fun WaitListContent(
) {
val title = stringResource(
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
}
)
val subtitle = when (state.loginAction) {
is Async.Success -> stringResource(
is AsyncData.Success -> stringResource(
id = R.string.screen_waitlist_message_success,
state.appName,
)
@@ -122,7 +122,7 @@ private fun OverallContent(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize()) {
if (state.loginAction !is Async.Success) {
if (state.loginAction !is AsyncData.Success) {
CompositionLocalProvider(LocalContentColor provides Color.Black) {
TextButton(
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(
text = stringResource(id = CommonStrings.action_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 io.element.android.features.login.impl.accountprovider.AccountProvider
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_URL
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
@@ -46,7 +46,7 @@ class ChangeServerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
}
}
@@ -61,13 +61,13 @@ class ChangeServerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
authenticationService.givenHomeserver(A_HOMESERVER)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
val loadingState = awaitItem()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
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()
}.test {
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)))
val loadingState = awaitItem()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
val failureState = awaitItem()
assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java)
assertThat(failureState.changeServerAction).isInstanceOf(AsyncData.Failure::class.java)
// Clear error
failureState.eventSink.invoke(ChangeServerEvents.ClearError)
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 com.google.common.truth.Truth.assertThat
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.auth.A_OIDC_DATA
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
@@ -49,7 +49,7 @@ class OidcPresenterTest {
}.test {
val initialState = awaitItem()
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()
initialState.eventSink.invoke(OidcEvents.Cancel)
val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
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()
initialState.eventSink.invoke(OidcEvents.Cancel)
val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
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.
}
}
@@ -104,9 +104,9 @@ class OidcPresenterTest {
val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack))
val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
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()
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
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.
}
}
@@ -141,12 +141,12 @@ class OidcPresenterTest {
val initialState = awaitItem()
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
val loadingState = awaitItem()
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
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)
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.oidc.customtab.DefaultOidcActionFlow
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.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
@@ -52,7 +52,7 @@ class ConfirmAccountProviderPresenterTest {
assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue()
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)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
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)
}
}
@@ -92,10 +92,10 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
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)
}
}
@@ -116,15 +116,15 @@ class ConfirmAccountProviderPresenterTest {
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
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)
authenticationService.givenOidcCancelError(A_THROWABLE)
defaultOidcActionFlow.post(OidcAction.GoBack)
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)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
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)
defaultOidcActionFlow.post(OidcAction.GoBack)
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)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
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)
authenticationService.givenLoginError(A_THROWABLE)
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val cancelLoadingState = awaitItem()
assertThat(cancelLoadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(cancelLoadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
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)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
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(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
val successSuccessState = awaitItem()
assertThat(successSuccessState.loginFlow).isInstanceOf(Async.Loading::class.java)
assertThat(successSuccessState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
}
}
@@ -233,7 +233,7 @@ class ConfirmAccountProviderPresenterTest {
skipItems(1) // Loading
val failureState = awaitItem()
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
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
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
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.accountprovider.AccountProviderDataSource
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.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_PASSWORD
@@ -57,7 +57,7 @@ class LoginPasswordPresenterTest {
val initialState = awaitItem()
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.submitEnabled).isFalse()
}
}
@@ -110,9 +110,9 @@ class LoginPasswordPresenterTest {
val loginAndPasswordState = awaitItem()
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
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()
}
}
@@ -139,9 +139,9 @@ class LoginPasswordPresenterTest {
authenticationService.givenLoginError(A_THROWABLE)
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
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)
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val loggedInState = awaitItem()
// 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
loggedInState.eventSink(LoginPasswordEvents.ClearError)
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.WellKnownBaseConfig
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.auth.FakeAuthenticationService
import io.element.android.tests.testutils.WarmUpRule
@@ -57,7 +57,7 @@ class SearchAccountProviderPresenterTest {
}.test {
val initialState = awaitItem()
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"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(Async.Uninitialized)
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(AsyncData.Uninitialized)
}
}
@@ -103,10 +103,10 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("https://test.org")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success(
AsyncData.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false)
)
@@ -138,10 +138,10 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success(
AsyncData.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false)
)
@@ -173,10 +173,10 @@ class SearchAccountProviderPresenterTest {
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(
Async.Success(
AsyncData.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test.io")
)

View File

@@ -22,7 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory
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.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
@@ -58,7 +58,7 @@ class WaitListPresenterTest {
val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo("Application Name")
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()
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
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
errorState.eventSink(WaitListEvents.ClearError)
val clearedState = awaitItem()
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized)
assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
}
}
@@ -113,9 +113,9 @@ class WaitListPresenterTest {
expectNoEvents()
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
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()
successState.eventSink.invoke(WaitListEvents.Continue)
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()

View File

@@ -16,11 +16,10 @@
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(
val canDoDirectSignOut: Boolean,
val showConfirmationDialog: Boolean,
val logoutAction: Async<String?>,
val logoutAction: AsyncAction<String?>,
val eventSink: (DirectLogoutEvents) -> Unit,
)

View File

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

View File

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

View File

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

View File

@@ -32,8 +32,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
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.LogoutConfirmationDialog
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.preview.ElementPreview
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(
state.logoutAction,
onConfirmClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
},
@@ -148,13 +138,13 @@ private fun ColumnScope.Buttons(
)
}
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
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading,
showProgress = logoutAction is AsyncAction.Loading,
destructive = true,
modifier = Modifier
.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.DirectLogoutState
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.di.SessionScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -54,8 +54,8 @@ class DefaultDirectLogoutPresenter @Inject constructor(
override fun present(): DirectLogoutState {
val localCoroutineScope = rememberCoroutineScope()
val logoutAction: MutableState<Async<String?>> = remember {
mutableStateOf(Async.Uninitialized)
val logoutAction: MutableState<AsyncAction<String?>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
@@ -70,7 +70,6 @@ class DefaultDirectLogoutPresenter @Inject constructor(
}
.collectAsState(initial = BackupUploadState.Unknown)
var showLogoutDialog by remember { mutableStateOf(false) }
var isLastSession by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
@@ -79,16 +78,14 @@ class DefaultDirectLogoutPresenter @Inject constructor(
fun handleEvents(event: DirectLogoutEvents) {
when (event) {
is DirectLogoutEvents.Logout -> {
if (showLogoutDialog || event.ignoreSdkError) {
showLogoutDialog = false
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
} else {
showLogoutDialog = true
logoutAction.value = AsyncAction.Confirming
}
}
DirectLogoutEvents.CloseDialogs -> {
logoutAction.value = Async.Uninitialized
showLogoutDialog = false
logoutAction.value = AsyncAction.Uninitialized
}
}
}
@@ -96,14 +93,13 @@ class DefaultDirectLogoutPresenter @Inject constructor(
return DirectLogoutState(
canDoDirectSignOut = !isLastSession &&
!backupUploadState.isBackingUp(),
showConfirmationDialog = showLogoutDialog,
logoutAction = logoutAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.logout(
logoutAction: MutableState<Async<String?>>,
logoutAction: MutableState<AsyncAction<String?>>,
ignoreSdkError: Boolean,
) = launch {
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.DirectLogoutView
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 javax.inject.Inject
@@ -34,20 +33,11 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
) {
val eventSink = state.eventSink
// Log out confirmation dialog
if (state.showConfirmationDialog) {
LogoutConfirmationDialog(
onSubmitClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onDismiss = {
eventSink(DirectLogoutEvents.CloseDialogs)
}
)
}
LogoutActionDialog(
state.logoutAction,
onConfirmClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
},
onForceLogoutClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
},

View File

@@ -20,22 +20,30 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource
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.dialogs.RetryDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LogoutActionDialog(
state: Async<String?>,
state: AsyncAction<String?>,
onConfirmClicked: () -> Unit,
onForceLogoutClicked: () -> Unit,
onDismissError: () -> Unit,
onDismissError: () -> Unit, // TODO Rename
onSuccessLogout: (String?) -> Unit,
) {
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))
is Async.Failure ->
is AsyncAction.Failure ->
RetryDialog(
title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.error_unknown),
@@ -43,9 +51,7 @@ fun LogoutActionDialog(
onRetry = onForceLogoutClicked,
onDismiss = onDismissError,
)
Async.Uninitialized ->
Unit
is Async.Success ->
is AsyncAction.Success ->
LaunchedEffect(state) {
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.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
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.test.FakeFeatureFlagService
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.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
@@ -50,14 +50,13 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
val initialState = awaitFirstItem()
assertThat(initialState.isLastSession).isFalse()
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
assertThat(initialState.doesBackupExistOnServer).isTrue()
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -75,8 +74,7 @@ class LogoutPresenterTest {
val initialState = awaitItem()
assertThat(initialState.isLastSession).isTrue()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -101,8 +99,7 @@ class LogoutPresenterTest {
val initialState = awaitItem()
assertThat(initialState.isLastSession).isFalse()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
skipItems(1)
val waitingState = awaitItem()
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
@@ -120,13 +117,13 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.showConfirmationDialog).isFalse()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -136,17 +133,15 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse()
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse()
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
skipItems(1)
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -191,28 +182,28 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse()
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
skipItems(1)
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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))
val loadingState2 = awaitItem()
assertThat(loadingState2.showConfirmationDialog).isFalse()
assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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(
matrixClient: MatrixClient = FakeMatrixClient(),
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.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents
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.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
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.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@@ -49,10 +48,9 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isTrue()
assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -69,8 +67,7 @@ class DefaultDirectLogoutPresenterTest {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -91,8 +88,7 @@ class DefaultDirectLogoutPresenterTest {
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -102,13 +98,13 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
val finalState = awaitItem()
assertThat(finalState.showConfirmationDialog).isFalse()
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -118,17 +114,15 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse()
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse()
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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)
val finalState = awaitItem()
assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@@ -172,27 +163,28 @@ class DefaultDirectLogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val initialState = awaitFirstItem()
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue()
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.showConfirmationDialog).isFalse()
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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))
val loadingState2 = awaitItem()
assertThat(loadingState2.showConfirmationDialog).isFalse()
assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java)
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
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(
matrixClient: MatrixClient = FakeMatrixClient(),
encryptionService: EncryptionService = FakeEncryptionService(),

View File

@@ -255,17 +255,21 @@ class MessagesFlowNode @AssistedInject constructor(
overlay.show(navTarget)
}
is TimelineItemStickerContent -> {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.body,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource,
)
overlay.show(navTarget)
/* Sticker may have an empty url and no thumbnail
if encrypted on certain bridges */
if (event.content.preferredMediaSource != null) {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.body,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
),
mediaSource = event.content.preferredMediaSource,
thumbnailSource = event.content.thumbnailSource,
)
overlay.show(navTarget)
}
}
is TimelineItemVideoContent -> {
val navTarget = NavTarget.MediaViewer(

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.preferences.api.store.PreferencesStore
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.core.coroutine.CoroutineDispatchers
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 userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value)
val roomName: Async<String> by remember {
derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized }
val roomName: AsyncData<String> by remember {
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
}
val roomAvatar: Async<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized }
val roomAvatar: AsyncData<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
}
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) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
withContext(dispatchers.io) {
@@ -279,8 +279,8 @@ class MessagesPresenter @AssistedInject constructor(
.onFailure { Timber.e(it) }
}
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) {
inviteProgress.value = Async.Loading()
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) {
inviteProgress.value = AsyncData.Loading()
runCatching {
room.updateMembers()
@@ -296,10 +296,10 @@ class MessagesPresenter @AssistedInject constructor(
}.getOrThrow()
}.fold(
onSuccess = {
inviteProgress.value = Async.Success(Unit)
inviteProgress.value = AsyncData.Success(Unit)
},
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.retrysendmenu.RetrySendMenuState
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.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
@@ -33,8 +33,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
data class MessagesState(
val roomId: RoomId,
val roomName: Async<String>,
val roomAvatar: Async<AvatarData>,
val roomName: AsyncData<String>,
val roomAvatar: AsyncData<AvatarData>,
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToRedact: Boolean,
val userHasPermissionToSendReaction: Boolean,
@@ -48,7 +48,7 @@ data class MessagesState(
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val inviteProgress: Async<Unit>,
val inviteProgress: AsyncData<Unit>,
val showReinvitePrompt: Boolean,
val enableTextFormatting: 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.voicemessages.composer.aVoiceMessageComposerState
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.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
@@ -47,8 +47,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
aMessagesState().copy(userHasPermissionToSendMessage = false),
aMessagesState().copy(showReinvitePrompt = true),
aMessagesState().copy(
roomName = Async.Uninitialized,
roomAvatar = Async.Uninitialized,
roomName = AsyncData.Uninitialized,
roomAvatar = AsyncData.Uninitialized,
),
aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)),
aMessagesState().copy(
@@ -83,8 +83,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
fun aMessagesState() = MessagesState(
roomId = RoomId("!id:domain"),
roomName = Async.Success("Room name"),
roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
roomName = AsyncData.Success("Room name"),
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage = true,
userHasPermissionToRedact = false,
userHasPermissionToSendReaction = true,
@@ -117,7 +117,7 @@ fun aMessagesState() = MessagesState(
),
hasNetworkConnection = true,
snackbarMessage = null,
inviteProgress = Async.Uninitialized,
inviteProgress = AsyncData.Uninitialized,
showReinvitePrompt = false,
enableTextFormatting = true,
enableVoiceMessages = true,

View File

@@ -152,7 +152,11 @@ class ActionListPresenter @Inject constructor(
add(TimelineItemAction.Reply)
}
}
add(TimelineItemAction.Forward)
// Stickers can't be forwarded (yet) so we don't show the option
// See https://github.com/element-hq/element-x-android/issues/2161
if (!timelineItem.isSticker) {
add(TimelineItemAction.Forward)
}
}
if (timelineItem.isMine && timelineItem.isTextMessage) {
add(TimelineItemAction.Edit)

View File

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

View File

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

View File

@@ -16,11 +16,11 @@
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(
val reason: String,
val blockUser: Boolean,
val result: Async<Unit>,
val result: AsyncAction<Unit>,
val eventSink: (ReportMessageEvents) -> Unit
)

View File

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

View File

@@ -41,8 +41,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.async.AsyncView
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -62,10 +62,10 @@ fun ReportMessageView(
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val isSending = state.result is Async.Loading
AsyncView(
val isSending = state.result is AsyncAction.Loading
AsyncActionView(
async = state.result,
showProgressDialog = false,
progressDialog = {},
onSuccess = { onBackClicked() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
onErrorDismiss = { state.eventSink(ReportMessageEvents.ClearError) }

View File

@@ -24,7 +24,9 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -41,6 +43,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider
@@ -52,7 +56,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
@Composable
@OptIn(ExperimentalFoundationApi::class)
@@ -114,8 +119,9 @@ sealed interface MessagesReactionsButtonContent {
val isHighlighted get() = this is Reaction && reaction.isHighlighted
}
private val reactionEmojiLineHeight = 20.sp
private val addEmojiSize = 16.dp
internal val REACTION_EMOJI_LINE_HEIGHT = 20.sp
internal const val REACTION_IMAGE_ASPECT_RATIO = 1.0f
private val ADD_EMOJI_SIZE = 16.dp
@Composable
private fun TextContent(
@@ -123,7 +129,7 @@ private fun TextContent(
modifier: Modifier = Modifier,
) = Text(
modifier = modifier
.height(reactionEmojiLineHeight.toDp()),
.height(REACTION_EMOJI_LINE_HEIGHT.toDp()),
text = text,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.materialColors.primary
@@ -138,7 +144,7 @@ private fun IconContent(
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
tint = ElementTheme.materialColors.secondary,
modifier = modifier
.size(addEmojiSize)
.size(ADD_EMOJI_SIZE)
)
@@ -150,13 +156,25 @@ private fun ReactionContent(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Text(
text = reaction.displayKey,
style = ElementTheme.typography.fontBodyMdRegular.copy(
fontSize = 15.sp,
lineHeight = reactionEmojiLineHeight,
),
)
// Check if this is a custom reaction (MSC4027)
if (reaction.key.startsWith("mxc://")) {
AsyncImage(
modifier = modifier
.heightIn(min = REACTION_EMOJI_LINE_HEIGHT.toDp(), max = REACTION_EMOJI_LINE_HEIGHT.toDp())
.aspectRatio(REACTION_IMAGE_ASPECT_RATIO, false),
model = MediaRequestData(MediaSource(reaction.key), MediaRequestData.Kind.Content),
contentDescription = null
)
}
else {
Text(
text = reaction.displayKey,
style = ElementTheme.typography.fontBodyMdRegular.copy(
fontSize = 15.sp,
lineHeight = REACTION_EMOJI_LINE_HEIGHT,
),
)
}
if (reaction.count > 1) {
Spacer(modifier = Modifier.width(4.dp))
Text(

View File

@@ -24,7 +24,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import org.jsoup.Jsoup
@PreviewsDayNight
@Composable
@@ -38,18 +37,15 @@ internal fun TimelineItemEventRowTimestampPreview(
"Text longer, displayed on 1 line",
"Text which should be rendered on several lines",
).forEach { str ->
listOf(false, true).forEach { useDocument ->
ATimelineItemEventRow(
event = event.copy(
content = oldContent.copy(
body = str,
htmlDocument = if (useDocument) Jsoup.parse(str) else null,
),
reactionsState = aTimelineItemReactions(count = 0),
senderDisplayName = if (useDocument) "Document case" else "Text case",
ATimelineItemEventRow(
event = event.copy(
content = oldContent.copy(
body = str,
),
)
}
reactionsState = aTimelineItemReactions(count = 0),
senderDisplayName = "A sender",
),
)
}
}
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.unit.Constraints
@@ -58,23 +59,18 @@ fun ContentAvoidingLayout(
) {
val scope = remember { ContentAvoidingLayoutScopeInstance() }
Layout(
SubcomposeLayout(
modifier = modifier,
content = {
scope.content()
overlay()
}
) { measurables, constraints ->
assert(measurables.size == 2) { "ContentAvoidingLayout must have exactly 2 children" }
) { constraints ->
// Measure the `overlay` view first, in case we need to shrink the `content`
val overlayPlaceable = measurables.last().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
val overlayPlaceable = subcompose(0, overlay).first().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
val contentConstraints = if (shrinkContent) {
Constraints(minWidth = 0, maxWidth = constraints.maxWidth - overlayPlaceable.width)
} else {
Constraints(minWidth = 0, maxWidth = constraints.maxWidth)
}
val contentPlaceable = measurables.first().measure(contentConstraints)
val contentPlaceable = subcompose(1) { scope.content() }.first().measure(contentConstraints)
var layoutWidth = contentPlaceable.width
var layoutHeight = contentPlaceable.height

View File

@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -57,20 +58,27 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.components.REACTION_IMAGE_ASPECT_RATIO
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
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.AvatarSize
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.compound.theme.ElementTheme
import kotlinx.coroutines.launch
internal val REACTION_SUMMARY_LINE_HEIGHT = 25.sp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReactionSummaryView(
@@ -192,13 +200,25 @@ private fun AggregatedReactionButton(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier,
) {
Text(
text = reaction.displayKey,
style = ElementTheme.typography.fontBodyMdRegular.copy(
fontSize = 20.sp,
lineHeight = 25.sp
),
)
// Check if this is a custom reaction (MSC4027)
if (reaction.key.startsWith("mxc://")) {
AsyncImage(
modifier = Modifier
.heightIn(min = REACTION_SUMMARY_LINE_HEIGHT.toDp(), max = REACTION_SUMMARY_LINE_HEIGHT.toDp())
.aspectRatio(REACTION_IMAGE_ASPECT_RATIO, false),
model = MediaRequestData(MediaSource(reaction.key), MediaRequestData.Kind.Content),
contentDescription = null
)
}
else {
Text(
text = reaction.displayKey,
style = ElementTheme.typography.fontBodyMdRegular.copy(
fontSize = 20.sp,
lineHeight = REACTION_SUMMARY_LINE_HEIGHT
),
)
}
if (reaction.count > 1) {
Spacer(modifier = Modifier.width(4.dp))
Text(
@@ -206,7 +226,7 @@ private fun AggregatedReactionButton(
color = textColor,
style = ElementTheme.typography.fontBodyMdRegular.copy(
fontSize = 20.sp,
lineHeight = 25.sp
lineHeight = REACTION_SUMMARY_LINE_HEIGHT
)
)
}

View File

@@ -72,7 +72,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
return when (val messageType = content.type) {
is EmoteMessageType -> {
val emoteBody = "* $senderDisplayName ${messageType.body}"
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
TimelineItemEmoteContent(
body = emoteBody,
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
@@ -83,7 +83,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is ImageMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemImageContent(
body = messageType.body,
body = messageType.body.trimEnd(),
mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@@ -98,7 +98,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is StickerMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemStickerContent(
body = messageType.body,
body = messageType.body.trimEnd(),
mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@@ -113,16 +113,17 @@ class TimelineItemContentMessageFactory @Inject constructor(
is LocationMessageType -> {
val location = Location.fromGeoUri(messageType.geoUri)
if (location == null) {
val body = messageType.body.trimEnd()
TimelineItemTextContent(
body = messageType.body,
body = body,
htmlDocument = null,
plainText = messageType.body,
plainText = body,
formattedBody = null,
isEdited = content.isEdited,
)
} else {
TimelineItemLocationContent(
body = messageType.body,
body = messageType.body.trimEnd(),
location = location,
description = messageType.description
)
@@ -131,7 +132,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is VideoMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemVideoContent(
body = messageType.body,
body = messageType.body.trimEnd(),
thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@@ -146,7 +147,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
}
is AudioMessageType -> {
TimelineItemAudioContent(
body = messageType.body,
body = messageType.body.trimEnd(),
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@@ -159,7 +160,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
true -> {
TimelineItemVoiceContent(
eventId = eventId,
body = messageType.body,
body = messageType.body.trimEnd(),
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@@ -168,7 +169,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
}
false -> {
TimelineItemAudioContent(
body = messageType.body,
body = messageType.body.trimEnd(),
mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@@ -181,7 +182,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
is FileMessageType -> {
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body)
TimelineItemFileContent(
body = messageType.body,
body = messageType.body.trimEnd(),
thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
@@ -189,26 +190,33 @@ class TimelineItemContentMessageFactory @Inject constructor(
fileExtension = fileExtension
)
}
is NoticeMessageType -> TimelineItemNoticeContent(
body = messageType.body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
isEdited = content.isEdited,
)
is TextMessageType -> {
TimelineItemTextContent(
body = messageType.body,
is NoticeMessageType -> {
val body = messageType.body.trimEnd()
TimelineItemNoticeContent(
body = body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
formattedBody = parseHtml(messageType.formatted) ?: messageType.body.withLinks(),
formattedBody = parseHtml(messageType.formatted) ?:body.withLinks(),
isEdited = content.isEdited,
)
}
is TextMessageType -> {
val body = messageType.body.trimEnd()
TimelineItemTextContent(
body = body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
isEdited = content.isEdited,
)
}
is OtherMessageType -> {
val body = messageType.body.trimEnd()
TimelineItemTextContent(
body = body,
htmlDocument = null,
formattedBody = body.withLinks(),
isEdited = content.isEdited,
)
}
is OtherMessageType -> TimelineItemTextContent(
body = messageType.body,
htmlDocument = null,
formattedBody = messageType.body.withLinks(),
isEdited = content.isEdited,
)
}
}
@@ -225,7 +233,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
private fun parseHtml(formattedBody: FormattedBody?, prefix: String? = null): CharSequence? {
if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null
val result = htmlConverterProvider.provide()
.fromHtmlToSpans(formattedBody.body)
.fromHtmlToSpans(formattedBody.body.trimEnd())
.withFixedURLSpans()
return if (prefix != null) {
buildSpannedString {

View File

@@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -81,6 +82,8 @@ sealed interface TimelineItem {
val isTextMessage: Boolean = content is TimelineItemTextBasedContent
val isSticker: Boolean = content is TimelineItemStickerContent
val isRemote = eventId != null
}

View File

@@ -16,7 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaSource
data class TimelineItemStickerContent(
@@ -33,9 +32,7 @@ data class TimelineItemStickerContent(
) : TimelineItemEventContent {
override val type: String = "TimelineItemStickerContent"
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
mediaSource
} else {
thumbnailSource ?: mediaSource
}
/* Stickers are supposed to be small images so
we allow using the mediaSource (unless the url is empty) */
val preferredMediaSource = if (mediaSource.url.isEmpty()) thumbnailSource else mediaSource
}

View File

@@ -42,6 +42,12 @@ import javax.inject.Inject
class MessageSummaryFormatterImpl @Inject constructor(
@ApplicationContext private val context: Context,
) : MessageSummaryFormatter {
companion object {
// Max characters to display in the summary message. This works around https://github.com/element-hq/element-x-android/issues/2105
private const val MAX_SAFE_LENGTH = 500
}
override fun format(event: TimelineItem.Event): String {
return when (event.content) {
is TimelineItemTextBasedContent -> event.content.plainText
@@ -58,6 +64,6 @@ class MessageSummaryFormatterImpl @Inject constructor(
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
is TimelineItemFileContent -> context.getString(CommonStrings.common_file)
is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio)
}
}.take(MAX_SAFE_LENGTH)
}
}

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.model.event.TimelineItemVoiceContent
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.runUpdatingState
import io.element.android.libraries.di.RoomScope
@@ -71,7 +71,7 @@ class VoiceMessagePresenter @AssistedInject constructor(
body = content.body,
)
private val play = mutableStateOf<Async<Unit>>(Async.Uninitialized)
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
@Composable
override fun present(): VoiceMessageState {
@@ -91,8 +91,8 @@ class VoiceMessagePresenter @AssistedInject constructor(
when {
content.eventId == null -> VoiceMessageState.Button.Disabled
playerState.isPlaying -> VoiceMessageState.Button.Pause
play.value is Async.Loading -> VoiceMessageState.Button.Downloading
play.value is Async.Failure -> VoiceMessageState.Button.Retry
play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
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.FakeSendPollResponseAction
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.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -117,14 +117,14 @@ class MessagesPresenterTest {
}.test {
val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
assertThat(initialState.roomName).isEqualTo(Async.Success(""))
assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
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.userHasPermissionToRedact).isFalse()
assertThat(initialState.hasNetworkConnection).isTrue()
assertThat(initialState.snackbarMessage).isNull()
assertThat(initialState.inviteProgress).isEqualTo(Async.Uninitialized)
assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.showReinvitePrompt).isFalse()
}
}

View File

@@ -20,7 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
@@ -45,7 +45,7 @@ class ReportMessagePresenterTests {
val initialState = awaitItem()
assertThat(initialState.reason).isEmpty()
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)
skipItems(1)
initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Success::class.java)
assertThat(room.reportedContentCount).isEqualTo(1)
}
}
@@ -106,8 +106,8 @@ class ReportMessagePresenterTests {
}.test {
val initialState = awaitItem()
initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Success::class.java)
assertThat(room.reportedContentCount).isEqualTo(1)
}
}
@@ -123,13 +123,13 @@ class ReportMessagePresenterTests {
}.test {
val initialState = awaitItem()
initialState.eventSink(ReportMessageEvents.Report)
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java)
assertThat(awaitItem().result).isInstanceOf(AsyncAction.Loading::class.java)
val resultState = awaitItem()
assertThat(resultState.result).isInstanceOf(Async.Failure::class.java)
assertThat(resultState.result).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(room.reportedContentCount).isEqualTo(1)
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.ComputeCacheSizeUseCase
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.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
@@ -65,10 +65,10 @@ class DeveloperSettingsPresenter @Inject constructor(
mutableStateMapOf<String, Boolean>()
}
val cacheSize = remember {
mutableStateOf<Async<String>>(Async.Uninitialized)
mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized)
}
val clearCacheAction = remember {
mutableStateOf<Async<Unit>>(Async.Uninitialized)
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
}
val customElementCallBaseUrl by preferencesStore
.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 {
computeCacheSizeUseCase()
}.runCatchingUpdatingState(cacheSize)
}
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncData<Unit>>) = launch {
suspend {
clearCacheUseCase()
}.runCatchingUpdatingState(clearCacheAction)

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