Merge branch 'develop' into feature/bma/sendImageFromKeyboard
This commit is contained in:
commit
cbc86ea1e4
759 changed files with 2603 additions and 1574 deletions
31
.github/workflows/fork-pr-notice.yml
vendored
Normal file
31
.github/workflows/fork-pr-notice.yml
vendored
Normal 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).`
|
||||||
|
})
|
||||||
6
.github/workflows/maestro.yml
vendored
6
.github/workflows/maestro.yml
vendored
|
|
@ -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):
|
||||||
|
|
|
||||||
6
.github/workflows/recordScreenshots.yml
vendored
6
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
|
||||||
22
.github/workflows/scripts/recordScreenshots.sh
vendored
22
.github/workflows/scripts/recordScreenshots.sh
vendored
|
|
@ -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!"
|
||||||
|
|
|
||||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
1
changelog.d/1949.bugfix
Normal 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
1
changelog.d/1949.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Render m.sticker events
|
||||||
1
changelog.d/2084.bugfix
Normal file
1
changelog.d/2084.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix room transition animation happens twice.
|
||||||
1
changelog.d/2099.bugfix
Normal file
1
changelog.d/2099.bugfix
Normal 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
1
changelog.d/2105.bugfix
Normal 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
1
changelog.d/2127.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Remove extra previews for timestamp view with 'document' case
|
||||||
1
changelog.d/2142.misc
Normal file
1
changelog.d/2142.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Bump AGP version to 8.2.0
|
||||||
1
changelog.d/2155.bugfix
Normal file
1
changelog.d/2155.bugfix
Normal 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
1
changelog.d/2159.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Added support for MSC4027 (render custom images in reactions)
|
||||||
1
changelog.d/2172.bugfix
Normal file
1
changelog.d/2172.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix no indication that user list is loading when inviting to room.
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>>)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)) },
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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 = { },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)) },
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue