Merge branch 'develop' into feature/bma/sendImageFromKeyboard

This commit is contained in:
Benoit Marty 2024-01-08 08:59:00 +01:00 committed by GitHub
commit cbc86ea1e4
759 changed files with 2603 additions and 1574 deletions

31
.github/workflows/fork-pr-notice.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Community PR notice
on:
workflow_dispatch:
pull_request_target:
types:
- opened
- reopened
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@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
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).`
})

View file

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

View file

@ -26,7 +26,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
with: with:
persist-credentials: false persist-credentials: false
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch) - name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
@ -43,8 +43,8 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots
id: record
run: ./.github/workflows/scripts/recordScreenshots.sh run: ./.github/workflows/scripts/recordScreenshots.sh
env: env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }}

View file

@ -68,11 +68,27 @@ echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn ./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn
echo "Committing changes" echo "Committing changes"
git config user.name "ElementBot" git config http.sslVerify false
git config user.email "benoitm+elementbot@element.io"
if [[ -z ${INPUT_AUTHOR_NAME} ]]; then
git config user.name "ElementBot"
else
git config --local user.name "${INPUT_AUTHOR_NAME}"
fi
if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then
git config user.email "benoitm+elementbot@element.io"
else
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi
git add -A git add -A
git commit -m "Update screenshots" git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
echo "Pushing changes" echo "Pushing changes"
git push "https://$TOKEN@github.com/$REPO.git" $BRANCH if [[ -z ${GITHUB_ACTOR} ]]; then
echo "No GITHUB_ACTOR env var"
GITHUB_REPO="https://$TOKEN@github.com/$REPO.git"
fi
git push $GITHUB_REPO "$BRANCH"
echo "Done!" echo "Done!"

View file

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

View file

@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -130,7 +131,9 @@ class RoomFlowNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
BackstackView() BackstackView(
transitionHandler = JumpToEndTransitionHandler(),
)
} }
} }

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/1949.feature Normal file
View file

@ -0,0 +1 @@
Render m.sticker events

1
changelog.d/2084.bugfix Normal file
View file

@ -0,0 +1 @@
Fix room transition animation happens twice.

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)

1
changelog.d/2172.bugfix Normal file
View file

@ -0,0 +1 @@
Fix no indication that user list is loading when inviting to room.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -35,6 +35,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.async.AsyncLoading
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@ -49,6 +50,7 @@ import kotlinx.collections.immutable.ImmutableList
fun SearchUserBar( fun SearchUserBar(
query: String, query: String,
state: SearchBarResultState<ImmutableList<UserSearchResult>>, state: SearchBarResultState<ImmutableList<UserSearchResult>>,
showLoader: Boolean,
selectedUsers: ImmutableList<MatrixUser>, selectedUsers: ImmutableList<MatrixUser>,
active: Boolean, active: Boolean,
isMultiSelectionEnabled: Boolean, isMultiSelectionEnabled: Boolean,
@ -99,6 +101,11 @@ fun SearchUserBar(
) )
} }
}, },
contentSuffix = {
if (showLoader) {
AsyncLoading()
}
},
resultState = state, resultState = state,
resultHandler = { users -> resultHandler = { users ->
LazyColumn(state = columnState) { LazyColumn(state = columnState) {

View file

@ -48,6 +48,7 @@ fun UserListView(
state = state.searchResults, state = state.searchResults,
selectedUsers = state.selectedUsers, selectedUsers = state.selectedUsers,
active = state.isSearchActive, active = state.isSearchActive,
showLoader = state.showSearchLoader,
isMultiSelectionEnabled = state.isMultiSelectionEnabled, isMultiSelectionEnabled = state.isMultiSelectionEnabled,
showBackButton = showBackButton, showBackButton = showBackButton,
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,6 +34,8 @@ import io.element.android.libraries.usersearch.api.UserRepository
import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class DefaultUserListPresenter @AssistedInject constructor( class DefaultUserListPresenter @AssistedInject constructor(
@Assisted val args: UserListPresenterArgs, @Assisted val args: UserListPresenterArgs,
@ -57,18 +59,21 @@ class DefaultUserListPresenter @AssistedInject constructor(
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList()) val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
var searchQuery by rememberSaveable { mutableStateOf("") } var searchQuery by rememberSaveable { mutableStateOf("") }
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember { var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
mutableStateOf(SearchBarResultState.NotSearching()) mutableStateOf(SearchBarResultState.Initial())
} }
var showSearchLoader by remember { mutableStateOf(false) }
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
searchResults = SearchBarResultState.NotSearching() searchResults = SearchBarResultState.Initial()
showSearchLoader = false
userRepository.search(searchQuery).collect { userRepository.search(searchQuery).onEach { state ->
showSearchLoader = state.isSearching
searchResults = when { searchResults = when {
it.isEmpty() -> SearchBarResultState.NoResults() state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial()
else -> SearchBarResultState.Results(it.toImmutableList()) state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound()
else -> SearchBarResultState.Results(state.results.toImmutableList())
} }
} }.launchIn(this)
} }
return UserListState( return UserListState(
@ -76,6 +81,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
searchResults = searchResults, searchResults = searchResults,
selectedUsers = selectedUsers.toImmutableList(), selectedUsers = selectedUsers.toImmutableList(),
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
showSearchLoader = showSearchLoader,
selectionMode = args.selectionMode, selectionMode = args.selectionMode,
eventSink = { event -> eventSink = { event ->
when (event) { when (event) {

View file

@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList
data class UserListState( data class UserListState(
val searchQuery: String, val searchQuery: String,
val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>, val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
val showSearchLoader: Boolean,
val selectedUsers: ImmutableList<MatrixUser>, val selectedUsers: ImmutableList<MatrixUser>,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val selectionMode: SelectionMode, val selectionMode: SelectionMode,

View file

@ -51,17 +51,19 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
aUserListState().copy( aUserListState().copy(
isSearchActive = true, isSearchActive = true,
searchQuery = "something-with-no-results", searchQuery = "something-with-no-results",
searchResults = SearchBarResultState.NoResults() searchResults = SearchBarResultState.NoResultsFound()
), ),
aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Single),
) )
} }
fun aUserListState() = UserListState( fun aUserListState() = UserListState(
isSearchActive = false, isSearchActive = false,
searchQuery = "", searchQuery = "",
searchResults = SearchBarResultState.NotSearching(), searchResults = SearchBarResultState.Initial(),
selectedUsers = persistentListOf(), selectedUsers = persistentListOf(),
selectionMode = SelectionMode.Single, selectionMode = SelectionMode.Single,
showSearchLoader = false,
eventSink = {} eventSink = {}
) )

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.api.UserSearchResult
import io.element.android.libraries.usersearch.api.UserSearchResultState
import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -55,7 +56,7 @@ class DefaultUserListPresenterTests {
assertThat(initialState.isMultiSelectionEnabled).isFalse() assertThat(initialState.isMultiSelectionEnabled).isFalse()
assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.isSearchActive).isFalse()
assertThat(initialState.selectedUsers).isEmpty() assertThat(initialState.selectedUsers).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
} }
} }
@ -76,7 +77,7 @@ class DefaultUserListPresenterTests {
assertThat(initialState.isMultiSelectionEnabled).isTrue() assertThat(initialState.isMultiSelectionEnabled).isTrue()
assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.isSearchActive).isFalse()
assertThat(initialState.selectedUsers).isEmpty() assertThat(initialState.selectedUsers).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
} }
} }
@ -131,25 +132,38 @@ class DefaultUserListPresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(userRepository.providedQuery).isEqualTo("alice") assertThat(userRepository.providedQuery).isEqualTo("alice")
skipItems(2) skipItems(2)
// When the user repository emits a result, it's copied to the state // When the user repository emits a result, it's copied to the state
userRepository.emitResult(listOf(UserSearchResult(aMatrixUser()))) val result = UserSearchResultState(
assertThat(awaitItem().searchResults).isEqualTo( results = listOf(UserSearchResult(aMatrixUser())),
SearchBarResultState.Results( isSearching = false,
persistentListOf(UserSearchResult(aMatrixUser()))
)
) )
userRepository.emitState(result)
awaitItem().also { state ->
assertThat(state.searchResults).isEqualTo(
SearchBarResultState.Results(
persistentListOf(UserSearchResult(aMatrixUser()))
)
)
assertThat(state.showSearchLoader).isFalse()
}
// When the user repository emits another result, it replaces the previous value // When the user repository emits another result, it replaces the previous value
userRepository.emitResult(aMatrixUserList().map { UserSearchResult(it) }) val newResult = UserSearchResultState(
assertThat(awaitItem().searchResults).isEqualTo( results = aMatrixUserList().map { UserSearchResult(it) },
SearchBarResultState.Results( isSearching = false,
aMatrixUserList().map { UserSearchResult(it) }
)
) )
userRepository.emitState(newResult)
awaitItem().also { state ->
assertThat(state.searchResults).isEqualTo(
SearchBarResultState.Results(
aMatrixUserList().map { UserSearchResult(it) }
)
)
assertThat(state.showSearchLoader).isFalse()
}
} }
} }
@ -170,13 +184,13 @@ class DefaultUserListPresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice")) initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
assertThat(userRepository.providedQuery).isEqualTo("alice") assertThat(userRepository.providedQuery).isEqualTo("alice")
skipItems(2) skipItems(2)
// When the results list is empty, the state is set to NoResults // When the results list is empty, the state is set to NoResults
userRepository.emitResult(emptyList()) userRepository.emitState(UserSearchResultState(results = emptyList(), isSearching = false))
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java) assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string> <string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string> <string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string> <string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App jedes Mal zu entsperren"</string> <string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren"</string>
<string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string> <string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string>
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string> <string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string> <string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.features.poll.api.create.CreatePollMode
@ -253,6 +254,23 @@ class MessagesFlowNode @AssistedInject constructor(
) )
overlay.show(navTarget) overlay.show(navTarget)
} }
is TimelineItemStickerContent -> {
/* 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 -> { is TimelineItemVideoContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(

View file

@ -54,6 +54,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
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.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
@ -64,7 +65,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.PreferencesStore import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@ -140,11 +141,11 @@ class MessagesPresenter @AssistedInject constructor(
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value) val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value) val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value)
val roomName: Async<String> by remember { val roomName: AsyncData<String> by remember {
derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized } derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
} }
val roomAvatar: Async<AvatarData> by remember { val roomAvatar: AsyncData<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized } derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
} }
var hasDismissedInviteDialog by rememberSaveable { var hasDismissedInviteDialog by rememberSaveable {
@ -161,7 +162,7 @@ class MessagesPresenter @AssistedInject constructor(
} }
} }
val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) } val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) { LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
@ -278,8 +279,8 @@ class MessagesPresenter @AssistedInject constructor(
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) { private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) {
inviteProgress.value = Async.Loading() inviteProgress.value = AsyncData.Loading()
runCatching { runCatching {
room.updateMembers() room.updateMembers()
@ -295,10 +296,10 @@ class MessagesPresenter @AssistedInject constructor(
}.getOrThrow() }.getOrThrow()
}.fold( }.fold(
onSuccess = { onSuccess = {
inviteProgress.value = Async.Success(Unit) inviteProgress.value = AsyncData.Success(Unit)
}, },
onFailure = { onFailure = {
inviteProgress.value = Async.Failure(it) inviteProgress.value = AsyncData.Failure(it)
} }
) )
} }
@ -351,6 +352,12 @@ class MessagesPresenter @AssistedInject constructor(
type = AttachmentThumbnailType.Image, type = AttachmentThumbnailType.Image,
blurHash = targetEvent.content.blurhash, blurHash = targetEvent.content.blurhash,
) )
is TimelineItemStickerContent -> AttachmentThumbnailInfo(
thumbnailSource = targetEvent.content.thumbnailSource ?: targetEvent.content.mediaSource,
textContent = targetEvent.content.body,
type = AttachmentThumbnailType.Image,
blurHash = targetEvent.content.blurhash,
)
is TimelineItemVideoContent -> AttachmentThumbnailInfo( is TimelineItemVideoContent -> AttachmentThumbnailInfo(
thumbnailSource = targetEvent.content.thumbnailSource, thumbnailSource = targetEvent.content.thumbnailSource,
textContent = targetEvent.content.body, textContent = targetEvent.content.body,

View file

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

View file

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

View file

@ -152,7 +152,11 @@ class ActionListPresenter @Inject constructor(
add(TimelineItemAction.Reply) 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) { if (timelineItem.isMine && timelineItem.isTextMessage) {
add(TimelineItemAction.Edit) add(TimelineItemAction.Edit)

View file

@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
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.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
@ -239,6 +240,9 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.body) }
} }
is TimelineItemStickerContent -> {
content = { ContentForBody(event.content.body) }
}
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.body) }
} }

View file

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

View file

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

View file

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

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