Merge remote-tracking branch 'origin/develop' into feature/fre/improve_poll_event_timeline_rendering
This commit is contained in:
commit
ebf6a98c34
71 changed files with 674 additions and 189 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
|
|
@ -17,13 +17,17 @@
|
||||||
|
|
||||||
## Screenshots / GIFs
|
## Screenshots / GIFs
|
||||||
|
|
||||||
<!-- Only if UI have been changed
|
<!--
|
||||||
|
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
|
||||||
|
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
|
||||||
|
Note that all the UI composables should be covered by a Composable Preview.
|
||||||
|
|
||||||
|
Providing a video of the change is still very useful for the reviewer and for the history of the project.
|
||||||
|
|
||||||
You can use a table like this to show screenshots comparison.
|
You can use a table like this to show screenshots comparison.
|
||||||
Uncomment this markdown table below and edit the last line `|||`:
|
Uncomment this markdown table below and edit the last line `|||`:
|
||||||
|copy screenshot of before here|copy screenshot of after here|
|
|copy screenshot of before here|copy screenshot of after here|
|
||||||
-->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|Before|After|
|
|Before|After|
|
||||||
|-|-|
|
|-|-|
|
||||||
|||
|
|||
|
||||||
|
|
@ -47,11 +51,11 @@ Uncomment this markdown table below and edit the last line `|||`:
|
||||||
|
|
||||||
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
|
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
|
||||||
|
|
||||||
- [ ] Changes has been tested on an Android device or Android emulator with API 21
|
- [ ] Changes have been tested on an Android device or Android emulator with API 23
|
||||||
- [ ] UI change has been tested on both light and dark themes
|
- [ ] UI change has been tested on both light and dark themes
|
||||||
- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#accessibility
|
- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
|
||||||
- [ ] Pull request is based on the develop branch
|
- [ ] Pull request is based on the develop branch
|
||||||
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog
|
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#changelog
|
||||||
- [ ] Pull request includes screenshots or videos if containing UI changes
|
- [ ] Pull request includes screenshots or videos if containing UI changes
|
||||||
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
|
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
|
||||||
- [ ] You've made a self review of your PR
|
- [ ] You've made a self review of your PR
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Contributing to Element Android
|
# Contributing to Element X Android
|
||||||
|
|
||||||
<!--- TOC -->
|
<!--- TOC -->
|
||||||
|
|
||||||
|
|
|
||||||
1
changelog.d/1111.bugfix
Normal file
1
changelog.d/1111.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Make links in messages clickable again.
|
||||||
1
changelog.d/1125.bugfix
Normal file
1
changelog.d/1125.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
When event has no id, just cancel parsing the latest room message for a room.
|
||||||
1
changelog.d/1127.feature
Normal file
1
changelog.d/1127.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Enable OIDC support.
|
||||||
1
changelog.d/1131.bugfix
Normal file
1
changelog.d/1131.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Only display verification prompt after initial sync is done.
|
||||||
1
changelog.d/862.bugfix
Normal file
1
changelog.d/862.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Videos sent from the app were cropped in some cases.
|
||||||
|
|
@ -45,3 +45,7 @@ state: ex6mNJVFZ5jn9wL8
|
||||||
|
|
||||||
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
|
||||||
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
|
||||||
|
|
||||||
|
|
||||||
|
Test server:
|
||||||
|
synapse-oidc.lab.element.dev
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -26,8 +27,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.features.login.api.oidc.OidcAction
|
||||||
|
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.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
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
|
||||||
|
|
@ -40,7 +44,9 @@ import java.net.URL
|
||||||
class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
@Assisted private val params: Params,
|
@Assisted private val params: Params,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val authenticationService: MatrixAuthenticationService
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
|
private val defaultOidcActionFlow: DefaultOidcActionFlow,
|
||||||
|
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||||
) : Presenter<ConfirmAccountProviderState> {
|
) : Presenter<ConfirmAccountProviderState> {
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
|
|
@ -61,6 +67,14 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch {
|
||||||
|
defaultOidcActionFlow.collect {
|
||||||
|
onOidcAction(it, loginFlowAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ConfirmAccountProviderEvents.Continue -> {
|
ConfirmAccountProviderEvents.Continue -> {
|
||||||
|
|
@ -97,4 +111,33 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
|
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun onOidcAction(
|
||||||
|
oidcAction: OidcAction?,
|
||||||
|
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||||
|
) {
|
||||||
|
oidcAction ?: return
|
||||||
|
loginFlowAction.value = Async.Loading()
|
||||||
|
when (oidcAction) {
|
||||||
|
OidcAction.GoBack -> {
|
||||||
|
authenticationService.cancelOidcLogin()
|
||||||
|
.onSuccess {
|
||||||
|
loginFlowAction.value = Async.Uninitialized
|
||||||
|
}
|
||||||
|
.onFailure { failure ->
|
||||||
|
loginFlowAction.value = Async.Failure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OidcAction.Success -> {
|
||||||
|
authenticationService.loginWithOidc(oidcAction.url)
|
||||||
|
.onSuccess { _ ->
|
||||||
|
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||||
|
}
|
||||||
|
.onFailure { failure ->
|
||||||
|
loginFlowAction.value = Async.Failure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultOidcActionFlow.reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
object LoginConstants {
|
object LoginConstants {
|
||||||
const val MATRIX_ORG_URL = "matrix.org"
|
const val MATRIX_ORG_URL = "matrix.org"
|
||||||
|
|
||||||
const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
|
const val DEFAULT_HOMESERVER_URL = "matrix.org"
|
||||||
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,24 +20,25 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
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.util.defaultAccountProvider
|
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
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
|
||||||
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.FakeAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||||
|
import io.element.android.tests.testutils.waitForPredicate
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ConfirmAccountProviderPresenterTest {
|
class ConfirmAccountProviderPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial test`() = runTest {
|
fun `present - initial test`() = runTest {
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter()
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
|
||||||
AccountProviderDataSource(),
|
|
||||||
FakeAuthenticationService(),
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -51,13 +52,11 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - continue password login`() = runTest {
|
fun `present - continue password login`() = runTest {
|
||||||
val authServer = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authServer,
|
|
||||||
)
|
)
|
||||||
authServer.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -75,13 +74,11 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - continue oidc`() = runTest {
|
fun `present - continue oidc`() = runTest {
|
||||||
val authServer = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authServer,
|
|
||||||
)
|
)
|
||||||
authServer.givenHomeserver(A_HOMESERVER_OIDC)
|
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -97,19 +94,135 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - oidc - cancel with failure`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||||
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
|
matrixAuthenticationService = authenticationService,
|
||||||
|
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
|
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
|
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||||
|
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||||
|
authenticationService.givenOidcCancelError(A_THROWABLE)
|
||||||
|
defaultOidcActionFlow.post(OidcAction.GoBack)
|
||||||
|
val cancelFailureState = awaitItem()
|
||||||
|
assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - oidc - cancel with success`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||||
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
|
matrixAuthenticationService = authenticationService,
|
||||||
|
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
|
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
|
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||||
|
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||||
|
defaultOidcActionFlow.post(OidcAction.GoBack)
|
||||||
|
val cancelFinalState = awaitItem()
|
||||||
|
assertThat(cancelFinalState.loginFlow).isInstanceOf(Async.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - oidc - success with failure`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||||
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
|
matrixAuthenticationService = authenticationService,
|
||||||
|
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
|
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
|
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||||
|
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||||
|
authenticationService.givenLoginError(A_THROWABLE)
|
||||||
|
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||||
|
val cancelLoadingState = awaitItem()
|
||||||
|
assertThat(cancelLoadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val cancelFailureState = awaitItem()
|
||||||
|
assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - oidc - success with success`() = runTest {
|
||||||
|
val authenticationService = FakeAuthenticationService()
|
||||||
|
val defaultOidcActionFlow = DefaultOidcActionFlow()
|
||||||
|
val defaultLoginUserStory = DefaultLoginUserStory().apply {
|
||||||
|
setLoginFlowIsDone(false)
|
||||||
|
}
|
||||||
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
|
matrixAuthenticationService = authenticationService,
|
||||||
|
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||||
|
defaultLoginUserStory = defaultLoginUserStory,
|
||||||
|
)
|
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
|
val loadingState = awaitItem()
|
||||||
|
assertThat(loadingState.submitEnabled).isTrue()
|
||||||
|
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.submitEnabled).isFalse()
|
||||||
|
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||||
|
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||||
|
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
|
||||||
|
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||||
|
val successSuccessState = awaitItem()
|
||||||
|
assertThat(successSuccessState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||||
|
waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - submit fails`() = runTest {
|
fun `present - submit fails`() = runTest {
|
||||||
val authServer = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authServer,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
authServer.givenChangeServerError(Throwable())
|
authenticationService.givenChangeServerError(Throwable())
|
||||||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||||
skipItems(1) // Loading
|
skipItems(1) // Loading
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
|
|
@ -121,10 +234,8 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - clear error`() = runTest {
|
fun `present - clear error`() = runTest {
|
||||||
val authenticationService = FakeAuthenticationService()
|
val authenticationService = FakeAuthenticationService()
|
||||||
val presenter = ConfirmAccountProviderPresenter(
|
val presenter = createConfirmAccountProviderPresenter(
|
||||||
ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
matrixAuthenticationService = authenticationService,
|
||||||
AccountProviderDataSource(),
|
|
||||||
authenticationService,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -147,4 +258,18 @@ class ConfirmAccountProviderPresenterTest {
|
||||||
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
|
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createConfirmAccountProviderPresenter(
|
||||||
|
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
|
||||||
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(),
|
||||||
|
matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(),
|
||||||
|
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
|
||||||
|
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),
|
||||||
|
) = ConfirmAccountProviderPresenter(
|
||||||
|
params = params,
|
||||||
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
|
authenticationService = matrixAuthenticationService,
|
||||||
|
defaultOidcActionFlow = defaultOidcActionFlow,
|
||||||
|
defaultLoginUserStory = defaultLoginUserStory,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutPreferenceView(
|
fun LogoutPreferenceView(
|
||||||
state: LogoutPreferenceState,
|
state: LogoutPreferenceState,
|
||||||
onSuccessLogout: () -> Unit = {}
|
onSuccessLogout: (String?) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
if (state.logoutAction is Async.Success) {
|
if (state.logoutAction is Async.Success) {
|
||||||
LaunchedEffect(state.logoutAction) {
|
LaunchedEffect(state.logoutAction) {
|
||||||
onSuccessLogout()
|
onSuccessLogout(state.logoutAction.data)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.logout.api
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
data class LogoutPreferenceState(
|
data class LogoutPreferenceState(
|
||||||
val logoutAction: Async<Unit>,
|
val logoutAction: Async<String?>,
|
||||||
val eventSink: (LogoutPreferenceEvents) -> Unit,
|
val eventSink: (LogoutPreferenceEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): LogoutPreferenceState {
|
override fun present(): LogoutPreferenceState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val logoutAction: MutableState<Async<Unit>> = remember {
|
val logoutAction: MutableState<Async<String?>> = remember {
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
|
private fun CoroutineScope.logout(logoutAction: MutableState<Async<String?>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout()
|
matrixClient.logout()
|
||||||
}.runCatchingUpdatingState(logoutAction)
|
}.runCatchingUpdatingState(logoutAction)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ fun TimelineItemTextView(
|
||||||
}
|
}
|
||||||
ClickableLinkText(
|
ClickableLinkText(
|
||||||
text = textWithPadding,
|
text = textWithPadding,
|
||||||
linkAnnotationTag = "URL",
|
|
||||||
onClick = onTextClicked,
|
onClick = onTextClicked,
|
||||||
onLongClick = onTextLongClicked,
|
onLongClick = onTextLongClicked,
|
||||||
interactionSource = interactionSource
|
interactionSource = interactionSource
|
||||||
|
|
|
||||||
|
|
@ -582,7 +582,6 @@ private fun HtmlText(
|
||||||
val inlineContentMap = persistentMapOf<String, InlineTextContent>()
|
val inlineContentMap = persistentMapOf<String, InlineTextContent>()
|
||||||
ClickableLinkText(
|
ClickableLinkText(
|
||||||
annotatedString = text,
|
annotatedString = text,
|
||||||
linkAnnotationTag = "URL",
|
|
||||||
style = style,
|
style = style,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
inlineContent = inlineContentMap,
|
inlineContent = inlineContentMap,
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@
|
||||||
<string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
|
<string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
|
||||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
|
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
|
||||||
<string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
|
<string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
|
||||||
|
<string name="screen_room_notification_settings_edit_remove_setting">"Remove custom setting"</string>
|
||||||
<string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
|
<string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
|
||||||
<string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
|
<string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
|
||||||
<string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
|
<string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
|
||||||
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
|
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
|
||||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||||
|
<string name="screen_room_notification_settings_room_custom_settings_title">"In this room, notify me for"</string>
|
||||||
<string name="screen_room_reactions_show_less">"Show less"</string>
|
<string name="screen_room_reactions_show_less">"Show less"</string>
|
||||||
<string name="screen_room_reactions_show_more">"Show more"</string>
|
<string name="screen_room_reactions_show_more">"Show more"</string>
|
||||||
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
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
|
||||||
|
|
@ -25,7 +27,9 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class PreferencesRootNode @AssistedInject constructor(
|
class PreferencesRootNode @AssistedInject constructor(
|
||||||
|
|
@ -62,9 +66,16 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onOpenAbout() }
|
plugins<Callback>().forEach { it.onOpenAbout() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onManageAccountClicked(activity: Activity, accountManagementUrl: String?) {
|
||||||
|
accountManagementUrl?.let {
|
||||||
|
activity.openUrlInChromeCustomTab(null, false, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
val activity = LocalContext.current as Activity
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -73,7 +84,16 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
onOpenAnalytics = this::onOpenAnalytics,
|
onOpenAnalytics = this::onOpenAnalytics,
|
||||||
onOpenAbout = this::onOpenAbout,
|
onOpenAbout = this::onOpenAbout,
|
||||||
onVerifyClicked = this::onVerifyClicked,
|
onVerifyClicked = this::onVerifyClicked,
|
||||||
onOpenDeveloperSettings = this::onOpenDeveloperSettings
|
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
|
||||||
|
onSuccessLogout = { onSuccessLogout(activity, it) },
|
||||||
|
onManageAccountClicked = { onManageAccountClicked(activity, state.accountManagementUrl) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSuccessLogout(activity: Activity, url: String?) {
|
||||||
|
Timber.d("Success logout with result url: $url")
|
||||||
|
url?.let {
|
||||||
|
activity.openUrlInChromeCustomTab(null, false, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -34,7 +33,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.user.getCurrentUser
|
import io.element.android.libraries.matrix.api.user.getCurrentUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -62,10 +60,15 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() }
|
val hasAnalyticsProviders = remember { analyticsService.getAvailableAnalyticsProviders().isNotEmpty() }
|
||||||
|
|
||||||
// Session verification status (unknown, not verified, verified)
|
// We should display the 'complete verification' option if the current session can be verified
|
||||||
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false)
|
||||||
val sessionIsNotVerified by remember {
|
|
||||||
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified }
|
val accountManagementUrl: MutableState<String?> = remember {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
initAccountManagementUrl(accountManagementUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
val logoutState = logoutPresenter.present()
|
val logoutState = logoutPresenter.present()
|
||||||
|
|
@ -74,7 +77,8 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
logoutState = logoutState,
|
logoutState = logoutState,
|
||||||
myUser = matrixUser.value,
|
myUser = matrixUser.value,
|
||||||
version = versionFormatter.get(),
|
version = versionFormatter.get(),
|
||||||
showCompleteVerification = sessionIsNotVerified,
|
showCompleteVerification = showCompleteVerification,
|
||||||
|
accountManagementUrl = accountManagementUrl.value,
|
||||||
showAnalyticsSettings = hasAnalyticsProviders,
|
showAnalyticsSettings = hasAnalyticsProviders,
|
||||||
showDeveloperSettings = showDeveloperSettings,
|
showDeveloperSettings = showDeveloperSettings,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
|
@ -84,4 +88,8 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
||||||
matrixUser.value = matrixClient.getCurrentUser()
|
matrixUser.value = matrixClient.getCurrentUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.initAccountManagementUrl(accountManagementUrl: MutableState<String?>) = launch {
|
||||||
|
accountManagementUrl.value = matrixClient.getAccountManagementUrl().getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ data class PreferencesRootState(
|
||||||
val myUser: MatrixUser?,
|
val myUser: MatrixUser?,
|
||||||
val version: String,
|
val version: String,
|
||||||
val showCompleteVerification: Boolean,
|
val showCompleteVerification: Boolean,
|
||||||
|
val accountManagementUrl: String?,
|
||||||
val showAnalyticsSettings: Boolean,
|
val showAnalyticsSettings: Boolean,
|
||||||
val showDeveloperSettings: Boolean,
|
val showDeveloperSettings: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ fun aPreferencesRootState() = PreferencesRootState(
|
||||||
myUser = null,
|
myUser = null,
|
||||||
version = "Version 1.1 (1)",
|
version = "Version 1.1 (1)",
|
||||||
showCompleteVerification = true,
|
showCompleteVerification = true,
|
||||||
|
accountManagementUrl = "aUrl",
|
||||||
showAnalyticsSettings = true,
|
showAnalyticsSettings = true,
|
||||||
showDeveloperSettings = true,
|
showDeveloperSettings = true,
|
||||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.material.icons.outlined.BugReport
|
||||||
import androidx.compose.material.icons.outlined.DeveloperMode
|
import androidx.compose.material.icons.outlined.DeveloperMode
|
||||||
import androidx.compose.material.icons.outlined.Help
|
import androidx.compose.material.icons.outlined.Help
|
||||||
import androidx.compose.material.icons.outlined.InsertChart
|
import androidx.compose.material.icons.outlined.InsertChart
|
||||||
|
import androidx.compose.material.icons.outlined.ManageAccounts
|
||||||
import androidx.compose.material.icons.outlined.VerifiedUser
|
import androidx.compose.material.icons.outlined.VerifiedUser
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -51,10 +52,12 @@ fun PreferencesRootView(
|
||||||
state: PreferencesRootState,
|
state: PreferencesRootState,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
onVerifyClicked: () -> Unit,
|
onVerifyClicked: () -> Unit,
|
||||||
|
onManageAccountClicked: () -> Unit,
|
||||||
onOpenAnalytics: () -> Unit,
|
onOpenAnalytics: () -> Unit,
|
||||||
onOpenRageShake: () -> Unit,
|
onOpenRageShake: () -> Unit,
|
||||||
onOpenAbout: () -> Unit,
|
onOpenAbout: () -> Unit,
|
||||||
onOpenDeveloperSettings: () -> Unit,
|
onOpenDeveloperSettings: () -> Unit,
|
||||||
|
onSuccessLogout: (String?) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
|
|
@ -75,6 +78,13 @@ fun PreferencesRootView(
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
|
if (state.accountManagementUrl != null) {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(id = CommonStrings.screen_settings_oidc_account),
|
||||||
|
icon = Icons.Outlined.ManageAccounts,
|
||||||
|
onClick = onManageAccountClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.showAnalyticsSettings) {
|
if (state.showAnalyticsSettings) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(id = CommonStrings.common_analytics),
|
title = stringResource(id = CommonStrings.common_analytics),
|
||||||
|
|
@ -98,6 +108,7 @@ fun PreferencesRootView(
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
LogoutPreferenceView(
|
LogoutPreferenceView(
|
||||||
state = state.logoutState,
|
state = state.logoutState,
|
||||||
|
onSuccessLogout = onSuccessLogout,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -140,5 +151,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||||
onOpenDeveloperSettings = {},
|
onOpenDeveloperSettings = {},
|
||||||
onOpenAbout = {},
|
onOpenAbout = {},
|
||||||
onVerifyClicked = {},
|
onVerifyClicked = {},
|
||||||
|
onSuccessLogout = {},
|
||||||
|
onManageAccountClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class PreferencesRootPresenterTest {
|
||||||
)
|
)
|
||||||
assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
|
assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
|
||||||
assertThat(loadedState.showAnalyticsSettings).isEqualTo(false)
|
assertThat(loadedState.showAnalyticsSettings).isEqualTo(false)
|
||||||
|
assertThat(loadedState.accountManagementUrl).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.user.getCurrentUser
|
import io.element.android.libraries.matrix.api.user.getCurrentUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -73,11 +72,11 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session verification status (unknown, not verified, verified)
|
// Session verification status (unknown, not verified, verified)
|
||||||
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
|
||||||
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
||||||
// We combine both values to only display the prompt if the session is not verified and it wasn't dismissed
|
// We combine both values to only display the prompt if the session is not verified and it wasn't dismissed
|
||||||
val displayVerificationPrompt by remember {
|
val displayVerificationPrompt by remember {
|
||||||
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
|
derivedStateOf { canVerifySession && !verificationPromptDismissed }
|
||||||
}
|
}
|
||||||
|
|
||||||
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
var displaySearchResults by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ class RoomListPresenterTests {
|
||||||
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
roomListService = roomListService
|
roomListService = roomListService,
|
||||||
)
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ test_core = "1.5.0"
|
||||||
#other
|
#other
|
||||||
coil = "2.4.0"
|
coil = "2.4.0"
|
||||||
datetime = "0.4.0"
|
datetime = "0.4.0"
|
||||||
serialization_json = "1.5.1"
|
serialization_json = "1.6.0"
|
||||||
showkase = "1.0.0-beta18"
|
showkase = "1.0.0-beta18"
|
||||||
jsoup = "1.16.1"
|
jsoup = "1.16.1"
|
||||||
appyx = "1.3.0"
|
appyx = "1.3.0"
|
||||||
|
|
@ -92,7 +92,7 @@ androidx_startup = { module = "androidx.startup:startup-runtime", version.ref =
|
||||||
androidx_preference = "androidx.preference:preference:1.2.1"
|
androidx_preference = "androidx.preference:preference:1.2.1"
|
||||||
|
|
||||||
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
|
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
|
||||||
androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha05"
|
androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha06"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
|
@ -146,7 +146,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.44"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.47"
|
||||||
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,15 @@ import io.element.android.libraries.theme.LinkColor
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
|
||||||
|
const val LINK_TAG = "URL"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableLinkText(
|
fun ClickableLinkText(
|
||||||
text: String,
|
text: String,
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
linkify: Boolean = true,
|
linkify: Boolean = true,
|
||||||
linkAnnotationTag: String = "",
|
linkAnnotationTag: String = LINK_TAG,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: () -> Unit = {},
|
onLongClick: () -> Unit = {},
|
||||||
style: TextStyle = LocalTextStyle.current,
|
style: TextStyle = LocalTextStyle.current,
|
||||||
|
|
@ -80,13 +82,14 @@ fun ClickableLinkText(
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
linkify: Boolean = true,
|
linkify: Boolean = true,
|
||||||
linkAnnotationTag: String = "",
|
linkAnnotationTag: String = LINK_TAG,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: () -> Unit = {},
|
onLongClick: () -> Unit = {},
|
||||||
style: TextStyle = LocalTextStyle.current,
|
style: TextStyle = LocalTextStyle.current,
|
||||||
inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(),
|
inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(),
|
||||||
) {
|
) {
|
||||||
val processedText = remember(annotatedString) {
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val annotatedString = remember(annotatedString) {
|
||||||
if (linkify) {
|
if (linkify) {
|
||||||
annotatedString.linkify(SpanStyle(color = LinkColor))
|
annotatedString.linkify(SpanStyle(color = LinkColor))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -126,7 +129,7 @@ fun ClickableLinkText(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = processedText,
|
text = annotatedString,
|
||||||
modifier = modifier.then(pressIndicator),
|
modifier = modifier.then(pressIndicator),
|
||||||
style = style,
|
style = style,
|
||||||
onTextLayout = {
|
onTextLayout = {
|
||||||
|
|
@ -158,7 +161,7 @@ fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
||||||
style = linkStyle,
|
style = linkStyle,
|
||||||
)
|
)
|
||||||
addStringAnnotation(
|
addStringAnnotation(
|
||||||
tag = "URL",
|
tag = LINK_TAG,
|
||||||
annotation = span.url,
|
annotation = span.url,
|
||||||
start = start,
|
start = start,
|
||||||
end = end
|
end = end
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -133,32 +134,46 @@ internal fun ButtonInternal(
|
||||||
ButtonSize.Large -> 48.dp
|
ButtonSize.Large -> 48.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasStartDrawable = showProgress || leadingIcon != null
|
||||||
|
|
||||||
val contentPadding = when (size) {
|
val contentPadding = when (size) {
|
||||||
ButtonSize.Medium -> {
|
ButtonSize.Medium -> when (style) {
|
||||||
when (style) {
|
ButtonStyle.Filled,
|
||||||
ButtonStyle.Text -> PaddingValues(horizontal = 12.dp, vertical = 10.dp)
|
ButtonStyle.Outlined -> if (hasStartDrawable)
|
||||||
else -> PaddingValues(horizontal = 16.dp, vertical = 10.dp)
|
PaddingValues(start = 16.dp, top = 10.dp, end = 24.dp, bottom = 10.dp)
|
||||||
}
|
else
|
||||||
|
PaddingValues(start = 24.dp, top = 10.dp, end = 24.dp, bottom = 10.dp)
|
||||||
|
ButtonStyle.Text -> if (hasStartDrawable)
|
||||||
|
PaddingValues(start = 12.dp, top = 10.dp, end = 16.dp, bottom = 10.dp)
|
||||||
|
else
|
||||||
|
PaddingValues(start = 12.dp, top = 10.dp, end = 12.dp, bottom = 10.dp)
|
||||||
}
|
}
|
||||||
ButtonSize.Large -> {
|
ButtonSize.Large -> when (style) {
|
||||||
when (style) {
|
ButtonStyle.Filled,
|
||||||
ButtonStyle.Text -> PaddingValues(horizontal = 16.dp, vertical = 13.dp)
|
ButtonStyle.Outlined -> if (hasStartDrawable)
|
||||||
else -> PaddingValues(horizontal = 24.dp, vertical = 13.dp)
|
PaddingValues(start = 24.dp, top = 13.dp, end = 32.dp, bottom = 13.dp)
|
||||||
}
|
else
|
||||||
|
PaddingValues(start = 32.dp, top = 13.dp, end = 32.dp, bottom = 13.dp)
|
||||||
|
ButtonStyle.Text -> if (hasStartDrawable)
|
||||||
|
PaddingValues(start = 12.dp, top = 13.dp, end = 16.dp, bottom = 13.dp)
|
||||||
|
else
|
||||||
|
PaddingValues(start = 16.dp, top = 13.dp, end = 16.dp, bottom = 13.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val shape = when (style) {
|
val shape = when (style) {
|
||||||
ButtonStyle.Filled, ButtonStyle.Outlined -> RoundedCornerShape(percent = 50)
|
ButtonStyle.Filled,
|
||||||
|
ButtonStyle.Outlined -> RoundedCornerShape(percent = 50)
|
||||||
ButtonStyle.Text -> RectangleShape
|
ButtonStyle.Text -> RectangleShape
|
||||||
}
|
}
|
||||||
|
|
||||||
val border = when (style) {
|
val border = when (style) {
|
||||||
ButtonStyle.Filled, ButtonStyle.Text -> null
|
ButtonStyle.Filled -> null
|
||||||
ButtonStyle.Outlined -> BorderStroke(
|
ButtonStyle.Outlined -> BorderStroke(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = ElementTheme.colors.borderInteractiveSecondary
|
color = ElementTheme.colors.borderInteractiveSecondary
|
||||||
)
|
)
|
||||||
|
ButtonStyle.Text -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
val textStyle = when (size) {
|
val textStyle = when (size) {
|
||||||
|
|
@ -166,11 +181,6 @@ internal fun ButtonInternal(
|
||||||
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
||||||
}
|
}
|
||||||
|
|
||||||
val internalPadding = when {
|
|
||||||
style == ButtonStyle.Text -> if (leadingIcon != null) PaddingValues(start = 8.dp) else PaddingValues(0.dp)
|
|
||||||
else -> PaddingValues(horizontal = 8.dp)
|
|
||||||
}
|
|
||||||
|
|
||||||
androidx.compose.material3.Button(
|
androidx.compose.material3.Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!showProgress) {
|
if (!showProgress) {
|
||||||
|
|
@ -195,6 +205,7 @@ internal fun ButtonInternal(
|
||||||
color = LocalContentColor.current,
|
color = LocalContentColor.current,
|
||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
leadingIcon != null -> {
|
leadingIcon != null -> {
|
||||||
androidx.compose.material.Icon(
|
androidx.compose.material.Icon(
|
||||||
|
|
@ -203,15 +214,14 @@ internal fun ButtonInternal(
|
||||||
tint = LocalContentColor.current,
|
tint = LocalContentColor.current,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = textStyle,
|
style = textStyle,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(internalPadding),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,15 @@ interface MatrixClient : Closeable {
|
||||||
* Will close the client and delete the cache data.
|
* Will close the client and delete the cache data.
|
||||||
*/
|
*/
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun logout()
|
|
||||||
|
/**
|
||||||
|
* Logout the user.
|
||||||
|
* Returns an optional URL. When the URL is there, it should be presented to the user after logout for RP initiated logout on their account page.
|
||||||
|
*/
|
||||||
|
suspend fun logout(): String?
|
||||||
suspend fun loadUserDisplayName(): Result<String>
|
suspend fun loadUserDisplayName(): Result<String>
|
||||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||||
|
suspend fun getAccountManagementUrl(): Result<String?>
|
||||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||||
fun roomMembershipObserver(): RoomMembershipObserver
|
fun roomMembershipObserver(): RoomMembershipObserver
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) {
|
||||||
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
||||||
class SessionMissing(message: String) : AuthenticationException(message)
|
class SessionMissing(message: String) : AuthenticationException(message)
|
||||||
class Generic(message: String) : AuthenticationException(message)
|
class Generic(message: String) : AuthenticationException(message)
|
||||||
// TODO Oidc
|
data class OidcError(val type: String, override val message: String) : AuthenticationException(message)
|
||||||
// class OidcError(type: String, message: String) : AuthenticationException(message)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -141,6 +142,21 @@ interface MatrixRoom : Closeable {
|
||||||
assetType: AssetType? = null,
|
assetType: AssetType? = null,
|
||||||
): Result<Unit>
|
): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a poll in the room.
|
||||||
|
*
|
||||||
|
* @param question The question to ask.
|
||||||
|
* @param answers The list of answers.
|
||||||
|
* @param maxSelections The maximum number of answers that can be selected.
|
||||||
|
* @param pollKind The kind of poll to create.
|
||||||
|
*/
|
||||||
|
suspend fun createPoll(
|
||||||
|
question: String,
|
||||||
|
answers: List<String>,
|
||||||
|
maxSelections: Int,
|
||||||
|
pollKind: PollKind,
|
||||||
|
): Result<Unit>
|
||||||
|
|
||||||
override fun close() = destroy()
|
override fun close() = destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.verification
|
package io.element.android.libraries.matrix.api.verification
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface SessionVerificationService {
|
interface SessionVerificationService {
|
||||||
|
|
@ -37,6 +38,11 @@ interface SessionVerificationService {
|
||||||
*/
|
*/
|
||||||
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
|
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the current session needs to be verified and the SDK is ready to start the verification.
|
||||||
|
*/
|
||||||
|
val canVerifySessionFlow: Flow<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request verification of the current session.
|
* Request verification of the current session.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,11 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||||
|
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||||
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
||||||
|
|
@ -92,8 +92,8 @@ class RustMatrixClient constructor(
|
||||||
private val innerRoomListService = syncService.roomListService()
|
private val innerRoomListService = syncService.roomListService()
|
||||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||||
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
|
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
|
||||||
private val verificationService = RustSessionVerificationService()
|
|
||||||
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
||||||
|
private val verificationService = RustSessionVerificationService(rustSyncService)
|
||||||
private val pushersService = RustPushersService(
|
private val pushersService = RustPushersService(
|
||||||
client = client,
|
client = client,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
|
|
@ -119,6 +119,13 @@ class RustMatrixClient constructor(
|
||||||
Timber.v("didReceiveAuthError -> already cleaning up")
|
Timber.v("didReceiveAuthError -> already cleaning up")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun didRefreshTokens() {
|
||||||
|
Timber.w("didRefreshTokens()")
|
||||||
|
appCoroutineScope.launch {
|
||||||
|
sessionStore.updateData(client.session().toSessionData())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val rustRoomListService: RoomListService =
|
private val rustRoomListService: RoomListService =
|
||||||
|
|
@ -141,13 +148,11 @@ class RustMatrixClient constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
client.setDelegate(clientDelegate)
|
client.setDelegate(clientDelegate)
|
||||||
rustSyncService.syncState
|
roomListService.state.onEach { state ->
|
||||||
.onEach { syncState ->
|
if (state == RoomListService.State.Running) {
|
||||||
if (syncState == SyncState.Running) {
|
setupVerificationControllerIfNeeded()
|
||||||
onSlidingSyncUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.launchIn(sessionCoroutineScope)
|
}.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
|
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
|
||||||
|
|
@ -287,21 +292,30 @@ class RustMatrixClient constructor(
|
||||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
|
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() = doLogout(doRequest = true)
|
override suspend fun logout(): String? = doLogout(doRequest = true)
|
||||||
|
|
||||||
private suspend fun doLogout(doRequest: Boolean) = withContext(sessionDispatcher) {
|
private suspend fun doLogout(doRequest: Boolean): String? {
|
||||||
if (doRequest) {
|
var result: String? = null
|
||||||
try {
|
withContext(sessionDispatcher) {
|
||||||
client.logout()
|
if (doRequest) {
|
||||||
} catch (failure: Throwable) {
|
try {
|
||||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
result = client.logout()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
close()
|
||||||
|
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
||||||
|
sessionStore.removeSession(sessionId.value)
|
||||||
}
|
}
|
||||||
close()
|
return result
|
||||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
|
||||||
sessionStore.removeSession(sessionId.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getAccountManagementUrl(): Result<String?> = withContext(sessionDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
client.accountUrl()
|
||||||
|
}
|
||||||
|
}
|
||||||
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
|
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
client.displayName()
|
client.displayName()
|
||||||
|
|
@ -321,8 +335,8 @@ class RustMatrixClient constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSlidingSyncUpdate() {
|
private fun setupVerificationControllerIfNeeded() {
|
||||||
if (!verificationService.isReady.value) {
|
if (verificationService.verificationController == null) {
|
||||||
try {
|
try {
|
||||||
verificationService.verificationController = client.getSessionVerificationController()
|
verificationService.verificationController = client.getSessionVerificationController()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|
|
||||||
|
|
@ -75,4 +75,5 @@ private fun SessionData.toSession() = Session(
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
|
oidcData = oidcData,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
||||||
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
||||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
||||||
|
|
||||||
/* TODO Oidc
|
|
||||||
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
||||||
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
||||||
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
||||||
is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!)
|
|
||||||
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
||||||
*/
|
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!)
|
||||||
|
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!)
|
||||||
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
|
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
|
||||||
MatrixHomeServerDetails(
|
MatrixHomeServerDetails(
|
||||||
url = url(),
|
url = url(),
|
||||||
supportsPasswordLogin = supportsPasswordLogin(),
|
supportsPasswordLogin = supportsPasswordLogin(),
|
||||||
supportsOidcLogin = false // TODO Oidc supportsOidcLogin(),
|
supportsOidcLogin = supportsOidcLogin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.auth
|
package io.element.android.libraries.matrix.impl.auth
|
||||||
|
|
||||||
// TODO Oidc
|
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||||
// import io.element.android.libraries.matrix.api.auth.OidcConfig
|
import org.matrix.rustcomponents.sdk.OidcConfiguration
|
||||||
// import org.matrix.rustcomponents.sdk.OidcClientMetadata
|
|
||||||
|
|
||||||
/*
|
val oidcConfiguration: OidcConfiguration = OidcConfiguration(
|
||||||
val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata(
|
|
||||||
clientName = "Element",
|
clientName = "Element",
|
||||||
redirectUri = OidcConfig.redirectUri,
|
redirectUri = OidcConfig.redirectUri,
|
||||||
clientUri = "https://element.io",
|
clientUri = "https://element.io",
|
||||||
tosUri = "https://element.io/user-terms-of-service",
|
tosUri = "https://element.io/user-terms-of-service",
|
||||||
policyUri = "https://element.io/privacy"
|
policyUri = "https://element.io/privacy",
|
||||||
|
/**
|
||||||
|
* Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
|
||||||
|
*/
|
||||||
|
staticRegistrations = mapOf(
|
||||||
|
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.auth
|
package io.element.android.libraries.matrix.impl.auth
|
||||||
|
|
||||||
// TODO Oidc
|
|
||||||
// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
import io.element.android.libraries.core.extensions.mapFailure
|
||||||
|
|
@ -30,17 +28,16 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||||
|
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Session
|
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
|
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
|
||||||
|
|
||||||
|
|
@ -57,9 +54,8 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
private val authService: RustAuthenticationService = RustAuthenticationService(
|
private val authService: RustAuthenticationService = RustAuthenticationService(
|
||||||
basePath = baseDirectory.absolutePath,
|
basePath = baseDirectory.absolutePath,
|
||||||
passphrase = null,
|
passphrase = null,
|
||||||
// TODO Oidc
|
|
||||||
// oidcClientMetadata = oidcClientMetadata,
|
|
||||||
userAgent = userAgentProvider.provide(),
|
userAgent = userAgentProvider.provide(),
|
||||||
|
oidcConfiguration = oidcConfiguration,
|
||||||
customSlidingSyncProxy = null,
|
customSlidingSyncProxy = null,
|
||||||
)
|
)
|
||||||
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||||
|
|
@ -112,68 +108,48 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Oidc
|
private var pendingOidcAuthenticationData: OidcAuthenticationData? = null
|
||||||
// private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null
|
|
||||||
|
|
||||||
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
||||||
TODO("Oidc")
|
|
||||||
/*
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val urlForOidcLogin = authService.urlForOidcLogin()
|
val oidcAuthenticationData = authService.urlForOidcLogin()
|
||||||
val url = urlForOidcLogin.loginUrl()
|
val url = oidcAuthenticationData.loginUrl()
|
||||||
pendingUrlForOidcLogin = urlForOidcLogin
|
pendingOidcAuthenticationData = oidcAuthenticationData
|
||||||
OidcDetails(url)
|
OidcDetails(url)
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancelOidcLogin(): Result<Unit> {
|
override suspend fun cancelOidcLogin(): Result<Unit> {
|
||||||
TODO("Oidc")
|
|
||||||
/*
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
pendingUrlForOidcLogin?.close()
|
pendingOidcAuthenticationData?.close()
|
||||||
pendingUrlForOidcLogin = null
|
pendingOidcAuthenticationData = null
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
|
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
|
||||||
*/
|
*/
|
||||||
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
||||||
TODO("Oidc")
|
|
||||||
/*
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first")
|
val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
|
||||||
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
||||||
val sessionData = client.use { it.session().toSessionData() }
|
val sessionData = client.use { it.session().toSessionData() }
|
||||||
pendingUrlForOidcLogin = null
|
pendingOidcAuthenticationData?.close()
|
||||||
|
pendingOidcAuthenticationData = null
|
||||||
sessionStore.storeData(sessionData)
|
sessionStore.storeData(sessionData)
|
||||||
SessionId(sessionData.userId)
|
SessionId(sessionData.userId)
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Session.toSessionData() = SessionData(
|
|
||||||
userId = userId,
|
|
||||||
deviceId = deviceId,
|
|
||||||
accessToken = accessToken,
|
|
||||||
refreshToken = refreshToken,
|
|
||||||
homeserverUrl = homeserverUrl,
|
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
|
||||||
loginTimestamp = Date(),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.mapper
|
||||||
|
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||||
|
import org.matrix.rustcomponents.sdk.Session
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
internal fun Session.toSessionData() = SessionData(
|
||||||
|
userId = userId,
|
||||||
|
deviceId = deviceId,
|
||||||
|
accessToken = accessToken,
|
||||||
|
refreshToken = refreshToken,
|
||||||
|
homeserverUrl = homeserverUrl,
|
||||||
|
oidcData = oidcData,
|
||||||
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
|
loginTimestamp = Date(),
|
||||||
|
)
|
||||||
|
|
@ -23,3 +23,8 @@ fun RustPollKind.map(): PollKind = when (this) {
|
||||||
RustPollKind.DISCLOSED -> PollKind.Disclosed
|
RustPollKind.DISCLOSED -> PollKind.Disclosed
|
||||||
RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
|
RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun PollKind.toInner(): RustPollKind = when (this) {
|
||||||
|
PollKind.Disclosed -> RustPollKind.DISCLOSED
|
||||||
|
PollKind.Undisclosed -> RustPollKind.UNDISCLOSED
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||||
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
||||||
import io.element.android.libraries.matrix.impl.media.map
|
import io.element.android.libraries.matrix.impl.media.map
|
||||||
|
import io.element.android.libraries.matrix.impl.poll.toInner
|
||||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||||
|
|
@ -378,7 +380,24 @@ class RustMatrixRoom(
|
||||||
description = description,
|
description = description,
|
||||||
zoomLevel = zoomLevel?.toUByte(),
|
zoomLevel = zoomLevel?.toUByte(),
|
||||||
assetType = assetType?.toInner(),
|
assetType = assetType?.toInner(),
|
||||||
txnId = genTransactionId()
|
txnId = genTransactionId(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createPoll(
|
||||||
|
question: String,
|
||||||
|
answers: List<String>,
|
||||||
|
maxSelections: Int,
|
||||||
|
pollKind: PollKind,
|
||||||
|
): Result<Unit> = withContext(roomDispatcher) {
|
||||||
|
runCatching {
|
||||||
|
innerRoom.createPoll(
|
||||||
|
question = question,
|
||||||
|
answers = answers,
|
||||||
|
maxSelections = maxSelections.toUByte(),
|
||||||
|
pollKind = pollKind.toInner(),
|
||||||
|
txnId = genTransactionId(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class RoomMessageFactory {
|
||||||
eventTimelineItem ?: return null
|
eventTimelineItem ?: return null
|
||||||
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
|
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
|
||||||
return RoomMessage(
|
return RoomMessage(
|
||||||
eventId = mappedTimelineItem.eventId!!,
|
eventId = mappedTimelineItem.eventId ?: return null,
|
||||||
event = mappedTimelineItem,
|
event = mappedTimelineItem,
|
||||||
sender = mappedTimelineItem.sender,
|
sender = mappedTimelineItem.sender,
|
||||||
originServerTs = mappedTimelineItem.timestamp,
|
originServerTs = mappedTimelineItem.timestamp,
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,25 @@
|
||||||
package io.element.android.libraries.matrix.impl.verification
|
package io.element.android.libraries.matrix.impl.verification
|
||||||
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
|
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationController
|
import org.matrix.rustcomponents.sdk.SessionVerificationController
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
|
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
|
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
|
||||||
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
|
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RustSessionVerificationService @Inject constructor() : SessionVerificationService, SessionVerificationControllerDelegate {
|
class RustSessionVerificationService @Inject constructor(
|
||||||
|
private val syncService: RustSyncService,
|
||||||
|
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
||||||
|
|
||||||
var verificationController: SessionVerificationControllerInterface? = null
|
var verificationController: SessionVerificationControllerInterface? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
@ -52,6 +57,10 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification
|
||||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
||||||
|
|
||||||
|
override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
|
||||||
|
syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun requestVerification() = tryOrFail {
|
override suspend fun requestVerification() = tryOrFail {
|
||||||
verificationController?.requestVerification()
|
verificationController?.requestVerification()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ class FakeMatrixClient(
|
||||||
private val pushersService: FakePushersService = FakePushersService(),
|
private val pushersService: FakePushersService = FakePushersService(),
|
||||||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||||
private val syncService: FakeSyncService = FakeSyncService(),
|
private val syncService: FakeSyncService = FakeSyncService(),
|
||||||
|
private val accountManagementUrlString: Result<String?> = Result.success(null),
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
|
|
||||||
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||||
|
|
@ -109,9 +110,10 @@ class FakeMatrixClient(
|
||||||
override suspend fun clearCache() {
|
override suspend fun clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() {
|
override suspend fun logout(): String? {
|
||||||
delay(100)
|
delay(100)
|
||||||
logoutFailure?.let { throw it }
|
logoutFailure?.let { throw it }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
|
@ -124,6 +126,9 @@ class FakeMatrixClient(
|
||||||
return userAvatarURLString
|
return userAvatarURLString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getAccountManagementUrl(): Result<String?> {
|
||||||
|
return accountManagementUrlString
|
||||||
|
}
|
||||||
override suspend fun uploadMedia(
|
override suspend fun uploadMedia(
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
data: ByteArray,
|
data: ByteArray,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
|
|
@ -83,6 +84,7 @@ class FakeMatrixRoom(
|
||||||
private var forwardEventResult = Result.success(Unit)
|
private var forwardEventResult = Result.success(Unit)
|
||||||
private var reportContentResult = Result.success(Unit)
|
private var reportContentResult = Result.success(Unit)
|
||||||
private var sendLocationResult = Result.success(Unit)
|
private var sendLocationResult = Result.success(Unit)
|
||||||
|
private var createPollResult = Result.success(Unit)
|
||||||
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||||
val editMessageCalls = mutableListOf<String>()
|
val editMessageCalls = mutableListOf<String>()
|
||||||
|
|
||||||
|
|
@ -104,6 +106,9 @@ class FakeMatrixRoom(
|
||||||
private val _sentLocations = mutableListOf<SendLocationInvocation>()
|
private val _sentLocations = mutableListOf<SendLocationInvocation>()
|
||||||
val sentLocations: List<SendLocationInvocation> = _sentLocations
|
val sentLocations: List<SendLocationInvocation> = _sentLocations
|
||||||
|
|
||||||
|
private val _createPollInvocations = mutableListOf<CreatePollInvocation>()
|
||||||
|
val createPollInvocations: List<CreatePollInvocation> = _createPollInvocations
|
||||||
|
|
||||||
var invitedUserId: UserId? = null
|
var invitedUserId: UserId? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -305,6 +310,16 @@ class FakeMatrixRoom(
|
||||||
return sendLocationResult
|
return sendLocationResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun createPoll(
|
||||||
|
question: String,
|
||||||
|
answers: List<String>,
|
||||||
|
maxSelections: Int,
|
||||||
|
pollKind: PollKind
|
||||||
|
): Result<Unit> = simulateLongTask {
|
||||||
|
_createPollInvocations.add(CreatePollInvocation(question, answers, maxSelections, pollKind))
|
||||||
|
return createPollResult
|
||||||
|
}
|
||||||
|
|
||||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||||
this.leaveRoomError = throwable
|
this.leaveRoomError = throwable
|
||||||
}
|
}
|
||||||
|
|
@ -397,6 +412,10 @@ class FakeMatrixRoom(
|
||||||
sendLocationResult = result
|
sendLocationResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCreatePollResult(result: Result<Unit>) {
|
||||||
|
createPollResult = result
|
||||||
|
}
|
||||||
|
|
||||||
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
|
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
|
||||||
progressCallbackValues = values
|
progressCallbackValues = values
|
||||||
}
|
}
|
||||||
|
|
@ -409,3 +428,10 @@ data class SendLocationInvocation(
|
||||||
val zoomLevel: Int?,
|
val zoomLevel: Int?,
|
||||||
val assetType: AssetType?,
|
val assetType: AssetType?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CreatePollInvocation(
|
||||||
|
val question: String,
|
||||||
|
val answers: List<String>,
|
||||||
|
val maxSelections: Int,
|
||||||
|
val pollKind: PollKind,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
|
@ -27,13 +28,13 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
private val _isReady = MutableStateFlow(false)
|
private val _isReady = MutableStateFlow(false)
|
||||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||||
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||||
|
private var _canVerifySessionFlow = MutableStateFlow(true)
|
||||||
private var emojiList = emptyList<VerificationEmoji>()
|
private var emojiList = emptyList<VerificationEmoji>()
|
||||||
var shouldFail = false
|
var shouldFail = false
|
||||||
|
|
||||||
override val verificationFlowState: StateFlow<VerificationFlowState>
|
override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState
|
||||||
get() = _verificationFlowState
|
|
||||||
|
|
||||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
||||||
|
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
|
||||||
|
|
||||||
override val isReady: StateFlow<Boolean> = _isReady
|
override val isReady: StateFlow<Boolean> = _isReady
|
||||||
|
|
||||||
|
|
@ -77,6 +78,10 @@ class FakeSessionVerificationService : SessionVerificationService {
|
||||||
_verificationFlowState.value = state
|
_verificationFlowState.value = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCanVerifySession(canVerify: Boolean) {
|
||||||
|
_canVerifySessionFlow.value = canVerify
|
||||||
|
}
|
||||||
|
|
||||||
fun givenIsReady(value: Boolean) {
|
fun givenIsReady(value: Boolean) {
|
||||||
_isReady.value = value
|
_isReady.value = value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.otaliastudios.transcoder.Transcoder
|
import com.otaliastudios.transcoder.Transcoder
|
||||||
import com.otaliastudios.transcoder.TranscoderListener
|
import com.otaliastudios.transcoder.TranscoderListener
|
||||||
|
import com.otaliastudios.transcoder.resize.AtMostResizer
|
||||||
|
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||||
import io.element.android.libraries.androidutils.file.createTmpFile
|
import io.element.android.libraries.androidutils.file.createTmpFile
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
import io.element.android.libraries.androidutils.file.safeDelete
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
|
@ -35,6 +37,11 @@ class VideoCompressor @Inject constructor(
|
||||||
fun compress(uri: Uri) = callbackFlow {
|
fun compress(uri: Uri) = callbackFlow {
|
||||||
val tmpFile = context.createTmpFile(extension = "mp4")
|
val tmpFile = context.createTmpFile(extension = "mp4")
|
||||||
val future = Transcoder.into(tmpFile.path)
|
val future = Transcoder.into(tmpFile.path)
|
||||||
|
.setVideoTrackStrategy(
|
||||||
|
DefaultVideoStrategy.Builder()
|
||||||
|
.addResizer(AtMostResizer(1920, 1080))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.addDataSource(context, uri)
|
.addDataSource(context, uri)
|
||||||
.setListener(object : TranscoderListener {
|
.setListener(object : TranscoderListener {
|
||||||
override fun onTranscodeProgress(progress: Double) {
|
override fun onTranscodeProgress(progress: Double) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ data class SessionData(
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
val refreshToken: String?,
|
val refreshToken: String?,
|
||||||
val homeserverUrl: String,
|
val homeserverUrl: String,
|
||||||
|
val oidcData: String?,
|
||||||
val slidingSyncProxy: String?,
|
val slidingSyncProxy: String?,
|
||||||
val loginTimestamp: Date?,
|
val loginTimestamp: Date?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ interface SessionStore {
|
||||||
fun isLoggedIn(): Flow<Boolean>
|
fun isLoggedIn(): Flow<Boolean>
|
||||||
fun sessionsFlow(): Flow<List<SessionData>>
|
fun sessionsFlow(): Flow<List<SessionData>>
|
||||||
suspend fun storeData(sessionData: SessionData)
|
suspend fun storeData(sessionData: SessionData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will update the session data matching the userId, except the value of loginTimestamp.
|
||||||
|
* No op if userId is not found in DB.
|
||||||
|
*/
|
||||||
|
suspend fun updateData(sessionData: SessionData)
|
||||||
suspend fun getSession(sessionId: String): SessionData?
|
suspend fun getSession(sessionId: String): SessionData?
|
||||||
suspend fun getAllSessions(): List<SessionData>
|
suspend fun getAllSessions(): List<SessionData>
|
||||||
suspend fun getLatestSession(): SessionData?
|
suspend fun getLatestSession(): SessionData?
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ class InMemorySessionStore : SessionStore {
|
||||||
sessionDataFlow.value = sessionData
|
sessionDataFlow.value = sessionData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateData(sessionData: SessionData) {
|
||||||
|
sessionDataFlow.value = sessionData
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getSession(sessionId: String): SessionData? {
|
override suspend fun getSession(sessionId: String): SessionData? {
|
||||||
return sessionDataFlow.value.takeIf { it?.userId == sessionId }
|
return sessionDataFlow.value.takeIf { it?.userId == sessionId }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,24 @@ class DatabaseSessionStore @Inject constructor(
|
||||||
database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
|
database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateData(sessionData: SessionData) {
|
||||||
|
val result = database.sessionDataQueries.selectByUserId(sessionData.userId)
|
||||||
|
.executeAsOneOrNull()
|
||||||
|
?.toApiModel()
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
Timber.e("User ${sessionData.userId} not found in session database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy new data from SDK, but keep login timestamp
|
||||||
|
database.sessionDataQueries.updateSession(
|
||||||
|
sessionData.copy(
|
||||||
|
loginTimestamp = result.loginTimestamp,
|
||||||
|
).toDbModel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getLatestSession(): SessionData? {
|
override suspend fun getLatestSession(): SessionData? {
|
||||||
return database.sessionDataQueries.selectFirst()
|
return database.sessionDataQueries.selectFirst()
|
||||||
.executeAsOneOrNull()
|
.executeAsOneOrNull()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
refreshToken = refreshToken,
|
refreshToken = refreshToken,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
|
oidcData = oidcData,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
loginTimestamp = loginTimestamp?.time,
|
loginTimestamp = loginTimestamp?.time,
|
||||||
)
|
)
|
||||||
|
|
@ -39,6 +40,7 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
||||||
accessToken = accessToken,
|
accessToken = accessToken,
|
||||||
refreshToken = refreshToken,
|
refreshToken = refreshToken,
|
||||||
homeserverUrl = homeserverUrl,
|
homeserverUrl = homeserverUrl,
|
||||||
|
oidcData = oidcData,
|
||||||
slidingSyncProxy = slidingSyncProxy,
|
slidingSyncProxy = slidingSyncProxy,
|
||||||
loginTimestamp = loginTimestamp?.let { Date(it) }
|
loginTimestamp = loginTimestamp?.let { Date(it) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ CREATE TABLE SessionData (
|
||||||
refreshToken TEXT,
|
refreshToken TEXT,
|
||||||
homeserverUrl TEXT NOT NULL,
|
homeserverUrl TEXT NOT NULL,
|
||||||
slidingSyncProxy TEXT,
|
slidingSyncProxy TEXT,
|
||||||
loginTimestamp INTEGER
|
loginTimestamp INTEGER,
|
||||||
|
oidcData TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,3 +24,6 @@ INSERT INTO SessionData VALUES ?;
|
||||||
|
|
||||||
removeSession:
|
removeSession:
|
||||||
DELETE FROM SessionData WHERE userId = ?;
|
DELETE FROM SessionData WHERE userId = ?;
|
||||||
|
|
||||||
|
updateSession:
|
||||||
|
REPLACE INTO SessionData VALUES ?;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE SessionData ADD COLUMN oidcData TEXT;
|
||||||
|
|
@ -37,6 +37,7 @@ class DatabaseSessionStoreTests {
|
||||||
homeserverUrl = "homeserverUrl",
|
homeserverUrl = "homeserverUrl",
|
||||||
slidingSyncProxy = null,
|
slidingSyncProxy = null,
|
||||||
loginTimestamp = null,
|
loginTimestamp = null,
|
||||||
|
oidcData = "aOidcData",
|
||||||
)
|
)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
@ -108,4 +109,45 @@ class DatabaseSessionStoreTests {
|
||||||
|
|
||||||
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
|
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update session update all fields except loginTimestamp`() = runTest {
|
||||||
|
val firstSessionData = SessionData(
|
||||||
|
userId = "userId",
|
||||||
|
deviceId = "deviceId",
|
||||||
|
accessToken = "accessToken",
|
||||||
|
refreshToken = "refreshToken",
|
||||||
|
homeserverUrl = "homeserverUrl",
|
||||||
|
slidingSyncProxy = "slidingSyncProxy",
|
||||||
|
loginTimestamp = 1,
|
||||||
|
oidcData = "aOidcData",
|
||||||
|
)
|
||||||
|
val secondSessionData = SessionData(
|
||||||
|
userId = "userId",
|
||||||
|
deviceId = "deviceIdAltered",
|
||||||
|
accessToken = "accessTokenAltered",
|
||||||
|
refreshToken = "refreshTokenAltered",
|
||||||
|
homeserverUrl = "homeserverUrlAltered",
|
||||||
|
slidingSyncProxy = "slidingSyncProxyAltered",
|
||||||
|
loginTimestamp = 2,
|
||||||
|
oidcData = "aOidcDataAltered",
|
||||||
|
)
|
||||||
|
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||||
|
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||||
|
|
||||||
|
database.sessionDataQueries.insertSessionData(firstSessionData)
|
||||||
|
databaseSessionStore.updateData(secondSessionData.toApiModel())
|
||||||
|
|
||||||
|
// Get the altered session
|
||||||
|
val alteredSession = databaseSessionStore.getSession(firstSessionData.userId)!!.toDbModel()
|
||||||
|
|
||||||
|
assertThat(alteredSession.userId).isEqualTo(secondSessionData.userId)
|
||||||
|
assertThat(alteredSession.deviceId).isEqualTo(secondSessionData.deviceId)
|
||||||
|
assertThat(alteredSession.accessToken).isEqualTo(secondSessionData.accessToken)
|
||||||
|
assertThat(alteredSession.refreshToken).isEqualTo(secondSessionData.refreshToken)
|
||||||
|
assertThat(alteredSession.homeserverUrl).isEqualTo(secondSessionData.homeserverUrl)
|
||||||
|
assertThat(alteredSession.slidingSyncProxy).isEqualTo(secondSessionData.slidingSyncProxy)
|
||||||
|
assertThat(alteredSession.loginTimestamp).isEqualTo(/* Not altered! */ firstSessionData.loginTimestamp)
|
||||||
|
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,11 +180,21 @@
|
||||||
<string name="screen_migration_title">"Setting up your account."</string>
|
<string name="screen_migration_title">"Setting up your account."</string>
|
||||||
<string name="screen_notification_settings_additional_settings_section_title">"Additional settings"</string>
|
<string name="screen_notification_settings_additional_settings_section_title">"Additional settings"</string>
|
||||||
<string name="screen_notification_settings_calls_label">"Audio and video calls"</string>
|
<string name="screen_notification_settings_calls_label">"Audio and video calls"</string>
|
||||||
|
<string name="screen_notification_settings_configuration_mismatch">"Configuration mismatch"</string>
|
||||||
|
<string name="screen_notification_settings_configuration_mismatch_description">"We’ve simplified Notifications Settings to make options easier to find.
|
||||||
|
|
||||||
|
Some custom settings you’ve chosen in the past are not shown here, but they’re still active.
|
||||||
|
|
||||||
|
If you proceed, some of your settings may change."</string>
|
||||||
<string name="screen_notification_settings_direct_chats">"Direct chats"</string>
|
<string name="screen_notification_settings_direct_chats">"Direct chats"</string>
|
||||||
|
<string name="screen_notification_settings_edit_custom_settings_section_title">"Custom setting per chat"</string>
|
||||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
||||||
|
<string name="screen_notification_settings_edit_mode_all_messages">"All messages"</string>
|
||||||
|
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"On direct chats, notify me for"</string>
|
<string name="screen_notification_settings_edit_screen_direct_section_header">"On direct chats, notify me for"</string>
|
||||||
<string name="screen_notification_settings_edit_screen_group_section_header">"On group chats, notify me for"</string>
|
<string name="screen_notification_settings_edit_screen_group_section_header">"On group chats, notify me for"</string>
|
||||||
<string name="screen_notification_settings_enable_notifications">"Enable notifications on this device"</string>
|
<string name="screen_notification_settings_enable_notifications">"Enable notifications on this device"</string>
|
||||||
|
<string name="screen_notification_settings_failed_fixing_configuration">"The configuration has not been corrected, please try again."</string>
|
||||||
<string name="screen_notification_settings_group_chats">"Group chats"</string>
|
<string name="screen_notification_settings_group_chats">"Group chats"</string>
|
||||||
<string name="screen_notification_settings_mentions_section_title">"Mentions"</string>
|
<string name="screen_notification_settings_mentions_section_title">"Mentions"</string>
|
||||||
<string name="screen_notification_settings_mode_all">"All"</string>
|
<string name="screen_notification_settings_mode_all">"All"</string>
|
||||||
|
|
@ -196,6 +206,7 @@
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
||||||
<string name="screen_notification_settings_title">"Notifications"</string>
|
<string name="screen_notification_settings_title">"Notifications"</string>
|
||||||
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||||
|
<string name="screen_settings_oidc_account">"Account and devices"</string>
|
||||||
<string name="screen_share_location_title">"Share location"</string>
|
<string name="screen_share_location_title">"Share location"</string>
|
||||||
<string name="screen_share_my_location_action">"Share my location"</string>
|
<string name="screen_share_my_location_action">"Share my location"</string>
|
||||||
<string name="screen_share_open_apple_maps">"Open in Apple Maps"</string>
|
<string name="screen_share_open_apple_maps">"Open in Apple Maps"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.tests.testutils
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
suspend fun waitForPredicate(
|
||||||
|
delayBetweenAttemptsMillis: Long = 1,
|
||||||
|
maxNumberOfAttempts: Int = 20,
|
||||||
|
predicate: () -> Boolean,
|
||||||
|
) {
|
||||||
|
for (i in 0..maxNumberOfAttempts) {
|
||||||
|
if (predicate()) return
|
||||||
|
if (i < maxNumberOfAttempts) delay(delayBetweenAttemptsMillis)
|
||||||
|
}
|
||||||
|
throw AssertionError("Predicate was not true after $maxNumberOfAttempts attempts")
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1c0cac586f62b725e9377111e3c2d39001048aebfd1f8fd113ea3e55d16d781c
|
oid sha256:6e902df5e96079c03fbbd36b79791c90f637ab29f4b2a2aa6df7981e09d93ee0
|
||||||
size 316907
|
size 316906
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8a8aab4a7fc06146668156c43424446d03ef8efb1c0ce1fd2105d45ced3e96d9
|
oid sha256:4de11f9a3d8a0165607960b926a2c1b14b6d0561ae52cbf504543bf0692fae09
|
||||||
size 310355
|
size 310356
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9b571751a70c761f4a460e0b486bd519faf30b08ec9ac208939d6690bc115874
|
oid sha256:0630d924f2bd374f8bd53ad41344b8d66ead984ce4bc0b55d455752d8d5409d9
|
||||||
size 405239
|
size 405238
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f0d500141b3cdf3410fddce876ee4c68d8ece26e40e34bd094cf95a7985ff199
|
oid sha256:77f91c56ce019ea31d618e63a5cba1c09d43140fd8b92458de2477e95a70ea7f
|
||||||
size 392950
|
size 392950
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2a80a93fd971e46b3a62c5e2679bfdab30cc01589398842346a2c7815af299fe
|
oid sha256:72eee76cc8244eb54f147fc589c7b200dc3a46db4ea7306dbd6757918e4fffde
|
||||||
size 35413
|
size 39744
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:49d6cc10dee437df8428616173de9b94b1b80c4b9edc72057e64b6dd51fa608d
|
oid sha256:8796e5f70cdd09087ed22ede78c3aed985dcd57e073f68d83dd5884e4f29a2c8
|
||||||
size 34701
|
size 39042
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:522926ef7065fab0176b686e49cf27ad29d4fd44d18d462cb156d45332cf368d
|
oid sha256:97250f48dfa0cf2320f837eb87ad90d8a1e73642fbb2d571f326d689c4fc10e0
|
||||||
size 37511
|
size 42373
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6aaddaf0c8b536ddc231c74b6bdcb22114fca8e7445e97af91c064333873af2c
|
oid sha256:73f2811197c014d91834d39dad1fe18abdaea9b2510d8deccead39c10a1b5aa8
|
||||||
size 37614
|
size 42473
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:02627b5036d3d937142e0086f5a50871fc760b787416e23f8b54e046c377060d
|
oid sha256:abe1134e30761c5ec6815c27568f0238b12f42c32746c69e428be477f28cc203
|
||||||
size 10251
|
size 10233
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ba3088e305d112ab0265c5d18005556c522f0b705bd2c547c23ebdd0b25c04ca
|
oid sha256:162a11f98fc16c2cf2de9380594bb29893c067479f5c9b2405b66b58474ae8c7
|
||||||
size 9849
|
size 9850
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f49f70bf1c036ad801002094642773cb569025d2d2eedb27a462dd3405dc0262
|
oid sha256:c6ac3ab7ac24942fca51adfc3cc8bf5ad797e912baf835baa73e8289b6d4a94d
|
||||||
size 28348
|
size 28308
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:83ae98e86b9e229c82e66e495ce9130f8aac3fd3364479409dd8aec2f60f5c44
|
oid sha256:d8233ab7e58175a8dbd14aa8c30dda882176099232cd56417102fa3675efe92b
|
||||||
size 32430
|
size 31114
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d8e9ce0aafcaa2c873d706c6e477f2103c8f930f0c15e186a981eaa831e51bfc
|
oid sha256:974dd693b78cc08e278ff81a65e234629cdbf1075d3fbf255895cf1ee2eb906c
|
||||||
size 30609
|
size 29264
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ baseAction = {
|
||||||
# Replacement done in all string values
|
# Replacement done in all string values
|
||||||
"replacements": {
|
"replacements": {
|
||||||
"...": "…"
|
"...": "…"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"force_underscore": "yes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue