Remove `SessionData.needsVerification` as the source of truth for session verification status

<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [ ] Feature
- [x] Bugfix
- [ ] Technical
- [ ] Other :

## Content

- Remove `SessionData.needsVerification` and its DB value.
- Use the Rust SDK `EncryptionService.verificationState()` instead, but always waiting for the first 'known' result (either verified or not, discarding 'unknown').
- Add a workaround in the super rare case when reading this value gets stuck somehow. We'll assume the user is not verified in that case.
- Make `DefaultFtueService.getNextStep` and dependent checks `suspend`.
- Make the `skip` button use a value in the session preferences instead.

## Motivation and context

Fixes #2718.

## Tests

<!-- Explain how you tested your development -->

With an already verified session:

- Open the app, if it doesn't crash the migration worked.
- You shouldn't be asked to verify again.
- Log out, then log in again. Verify using either another device or a recovery key (ideally both).

The verification flow should work just as before.

## Tested devices

- [x] Physical
- [x] Emulator
- OS version(s): 14, 13.

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] 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
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [x] Pull request is based on the develop branch
- [x] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] 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)
- [x] You've made a self review of your PR

Verified Element X session became unverified
### Steps to reproduce

1. Setup Element X.
2. Login.
3. Verify the session with another device.
4. Use your "Recovery Key" to restore key backup.
5. wait a day :P

### Outcome

#### What did you expect?
Things to work normally-ish.

#### What happened instead?

Today, my Element X started thinking it was unverified *despite continuing to receive and see (readable) DMs from an encrypted room in my Android notification tray*.

#### Timeline

* Apr 15 - ??? unknown time, likely around 19:00 - I reset Key Backup because it had been busted for quite a while. I ensured that I'd gotten as many keys as possible into my session, reset it.
* Apr 15 - 19:2x - I start setting up Element X Android
* Apr 15 - 19:2x - I confirm Element X Android session, using Element Web
* Apr 15 - 19:2x -I noticed that I'm prompted for recovery key (maybe to join KeyBackup), despite session verification
* Apr 15 - 19:28 - I message the Element X Android room saying I successfully got a verified Element X session

* Apr 16 - 15:36 - I randomly look at Element Web, and notice that Element X Android **is not appearing in my session list _at all_**. Later, when it does re-appear, I realize in retrospect that at least one of my other verified clients had also been missing from the list.

* Apr 16  - approx  17:15:  I'm walking outside, and click on one of the notifications on my phone (possibly the first time doing so today, I probably was just reading them on Element Web while I worked). Almost immediately it popped up and asked me to Verify the session.

* Apr 16  - approx  17:40:  element x **DOES** appear in my verified sessions in Element Web

* Apr 16 - approx 17:44: element x android is  ... again not appearing in my verified sessions in Element Web.

Extra details:
* I only have one element web session\
* It seems that clicking the "Inactive sessions" in the top part of the Sessions window *filters* the list below. Which is awesome, makes total sense. I am now nervous that that's why I wasn't seeing Element X in the list? But... I'm still not sure.

### Your phone model

Google Pixel 6a

### Operating system version

Android 14 latest

### Application version and app store

0.4.9 (40004090)

### Homeserver

matrix.org

### Will you send logs?

Yes

### Are you willing to provide a PR?

Yes

Some days ago, I also got the verification screen on my phone. I wonder whether that was just caused by a recent update that (I think) changed the verification process/screen.

So possibly, the session never really was unverified, just the verification screen shows up.

@bmarty, I think, you did that verification workflow commits; possibly you can check whether my theory is correct.
I am talking about #2580. But that wouldn't really add up time-wise, as that is a bit older.
We are forcing users to verify their session in the coming release (database migration here: https://github.com/element-hq/element-x-android/issues/2718), but it seems that some code to set the real verification status is missing.

For the rationale about mandatory verification please refer to https://github.com/element-hq/element-x-android/issues/2702#issuecomment-2069121338
The broader discussion is a bit over my head (though I appreciate the insight) - but I think your DB migration link is wrong (links to this issue).

Also, I'm totally on board with mandatory verification, especially if y'all get QR login going (which, makes me curious how thats implemented, :crossed_fingers: it's via OIDC/OAuth-y mechanisms but idk if it's too early for that).

Sounds like y'all are on it, but for what it's worth, I haven't had any issues with Element X Android since getting it "re"-verified.
@bmarty, the thing is that it appears that already verified sessions were forced to verify again.

diff --git a/changelog.d/2718.bugfix b/changelog.d/2718.bugfix
new file mode 100644
index 00000000000..999d47b59ba
--- /dev/null
+++ b/changelog.d/2718.bugfix
@@ -0,0 +1,1 @@
+Fix session verification being asked again for already verified users.
diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts
index 06251cfe5dc..e42763d97cc 100644
--- a/features/ftue/impl/build.gradle.kts
+++ b/features/ftue/impl/build.gradle.kts
@@ -39,6 +39,7 @@ dependencies {
     implementation(projects.libraries.matrix.api)
     implementation(projects.libraries.matrixui)
     implementation(projects.libraries.designsystem)
+    implementation(projects.libraries.preferences.api)
     implementation(projects.libraries.uiStrings)
     implementation(projects.libraries.testtags)
     implementation(projects.features.analytics.api)
@@ -60,6 +61,7 @@ dependencies {
     testImplementation(projects.services.analytics.test)
     testImplementation(projects.libraries.permissions.impl)
     testImplementation(projects.libraries.permissions.test)
+    testImplementation(projects.libraries.preferences.test)
     testImplementation(projects.features.lockscreen.test)
     testImplementation(projects.tests.testutils)

diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt
index cb77100d94e..3616faf2aec 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt
@@ -143,7 +143,7 @@ class FtueFlowNode @AssistedInject constructor(
         }
     }

-    private fun moveToNextStep() {
+    private fun moveToNextStep() = lifecycleScope.launch {
         when (ftueState.getNextStep()) {
             FtueStep.SessionVerification -> {
                 backstack.newRoot(NavTarget.SessionVerification)
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt
index 87b7c3a7ee2..0e8aa54f819 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt
@@ -23,18 +23,26 @@ import com.squareup.anvil.annotations.ContributesBinding
 import io.element.android.features.ftue.api.state.FtueService
 import io.element.android.features.ftue.api.state.FtueState
 import io.element.android.features.lockscreen.api.LockScreenService
+import io.element.android.features.preferences.api.store.SessionPreferencesStore
 import io.element.android.libraries.di.SessionScope
 import io.element.android.libraries.matrix.api.verification.SessionVerificationService
+import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
 import io.element.android.libraries.permissions.api.PermissionStateProvider
 import io.element.android.services.analytics.api.AnalyticsService
 import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.timeout
 import kotlinx.coroutines.runBlocking
+import timber.log.Timber
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds

 @ContributesBinding(SessionScope::class)
 class DefaultFtueService @Inject constructor(
@@ -44,6 +52,7 @@ class DefaultFtueService @Inject constructor(
     private val permissionStateProvider: PermissionStateProvider,
     private val lockScreenService: LockScreenService,
     private val sessionVerificationService: SessionVerificationService,
+    private val sessionPreferencesStore: SessionPreferencesStore,
 ) : FtueService {
     override val state = MutableStateFlow<FtueState>(FtueState.Unknown)

@@ -55,7 +64,7 @@ class DefaultFtueService @Inject constructor(
     }

     init {
-        sessionVerificationService.needsVerificationFlow
+        sessionVerificationService.sessionVerifiedStatus
             .onEach { updateState() }
             .launchIn(coroutineScope)

@@ -64,7 +73,7 @@ class DefaultFtueService @Inject constructor(
             .launchIn(coroutineScope)
     }

-    fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
+    suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
         when (currentStep) {
             null -> if (isSessionNotVerified()) {
                 FtueStep.SessionVerification
@@ -89,8 +98,8 @@ class DefaultFtueService @Inject constructor(
             FtueStep.AnalyticsOptIn -> null
         }

-    private fun isAnyStepIncomplete(): Boolean {
-        return listOf(
+    private suspend fun isAnyStepIncomplete(): Boolean {
+        return listOf<suspend () -> Boolean>(
             { isSessionNotVerified() },
             { shouldAskNotificationPermissions() },
             { needsAnalyticsOptIn() },
@@ -98,16 +107,28 @@ class DefaultFtueService @Inject constructor(
         ).any { it() }
     }

-    private fun isSessionNotVerified(): Boolean {
-        return sessionVerificationService.needsVerificationFlow.value
+    @OptIn(FlowPreview::class)
+    private suspend fun isSessionNotVerified(): Boolean {
+        // Wait for the first known (or ready) verification status
+        val readyVerifiedSessionStatus = sessionVerificationService.sessionVerifiedStatus
+            .filter { it != SessionVerifiedStatus.Unknown }
+            // This is not ideal, but there are some very rare cases when reading the flow seems to get stuck
+            .timeout(5.seconds)
+            .catch {
+                Timber.e(it, "Failed to get session verification status, assume it's not verified")
+                emit(SessionVerifiedStatus.NotVerified)
+            }
+            .first()
+        val skipVerification = suspend { sessionPreferencesStore.isSessionVerificationSkipped().first() }
+        return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !skipVerification()
     }

-    private fun needsAnalyticsOptIn(): Boolean {
+    private suspend fun needsAnalyticsOptIn(): Boolean {
         // We need this function to not be suspend, so we need to load the value through runBlocking
-        return runBlocking { analyticsService.didAskUserConsent().first().not() }
+        return analyticsService.didAskUserConsent().first().not()
     }

-    private fun shouldAskNotificationPermissions(): Boolean {
+    private suspend fun shouldAskNotificationPermissions(): Boolean {
         return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
             val permission = Manifest.permission.POST_NOTIFICATIONS
             val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
@@ -118,14 +139,12 @@ class DefaultFtueService @Inject constructor(
         }
     }

-    private fun shouldDisplayLockscreenSetup(): Boolean {
-        return runBlocking {
-            lockScreenService.isSetupRequired().first()
-        }
+    private suspend fun shouldDisplayLockscreenSetup(): Boolean {
+        return lockScreenService.isSetupRequired().first()
     }

     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    internal fun updateState() {
+    internal suspend fun updateState() {
         state.value = when {
             isAnyStepIncomplete() -> FtueState.Incomplete
             else -> FtueState.Complete
diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt
index 9827d8d720f..df3549b0f81 100644
--- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt
+++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt
@@ -38,7 +38,6 @@ fun aSignedOutState() = SignedOutState(
 fun aSessionData(
     sessionId: SessionId = SessionId("@alice:server.org"),
     isTokenValid: Boolean = false,
-    needsVerification: Boolean = false,
 ): SessionData {
     return SessionData(
         userId = sessionId.value,
@@ -52,6 +51,5 @@ fun aSessionData(
         isTokenValid = isTokenValid,
         loginType = LoginType.UNKNOWN,
         passphrase = null,
-        needsVerification = needsVerification,
     )
 }
diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts
index 3f62c6898e3..38cb453a057 100644
--- a/features/verifysession/impl/build.gradle.kts
+++ b/features/verifysession/impl/build.gradle.kts
@@ -42,6 +42,7 @@ dependencies {
     implementation(projects.libraries.matrix.api)
     implementation(projects.libraries.matrixui)
     implementation(projects.libraries.designsystem)
+    implementation(projects.libraries.preferences.api)
     implementation(projects.libraries.uiStrings)
     api(libs.statemachine)
     api(projects.features.verifysession.api)
@@ -53,6 +54,7 @@ dependencies {
     testImplementation(libs.test.truth)
     testImplementation(libs.test.turbine)
     testImplementation(projects.libraries.matrix.test)
+    testImplementation(projects.libraries.preferences.test)
     testImplementation(projects.tests.testutils)
     testImplementation(libs.androidx.compose.ui.test.junit)
     testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
index c09b48946d4..e414f1b7460 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
@@ -23,11 +23,11 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import com.freeletics.flowredux.compose.rememberStateAndDispatch
+import io.element.android.features.preferences.api.store.SessionPreferencesStore
 import io.element.android.libraries.architecture.AsyncData
 import io.element.android.libraries.architecture.Presenter
 import io.element.android.libraries.core.meta.BuildMeta
@@ -49,6 +49,7 @@ class VerifySelfSessionPresenter @Inject constructor(
     private val encryptionService: EncryptionService,
     private val stateMachine: VerifySelfSessionStateMachine,
     private val buildMeta: BuildMeta,
+    private val sessionPreferencesStore: SessionPreferencesStore,
 ) : Presenter<VerifySelfSessionState> {
     @Composable
     override fun present(): VerifySelfSessionState {
@@ -59,8 +60,8 @@ class VerifySelfSessionPresenter @Inject constructor(
         }
         val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
         val stateAndDispatch = stateMachine.rememberStateAndDispatch()
-        var skipVerification by remember { mutableStateOf(false) }
-        val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState()
+        val skipVerification by sessionPreferencesStore.isSessionVerificationSkipped().collectAsState(initial = false)
+        val needsVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = true)
         val verificationFlowStep by remember {
             derivedStateOf {
                 when {
@@ -86,8 +87,7 @@ class VerifySelfSessionPresenter @Inject constructor(
                 VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
                 VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
                 VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
-                    sessionVerificationService.saveVerifiedState(true)
-                    skipVerification = true
+                    sessionPreferencesStore.setSkipSessionVerification(true)
                 }
             }
         }
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
index b82dd231881..8d22fc174e0 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
@@ -21,17 +21,6 @@ import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow

 interface SessionVerificationService {
-    /**
-     * This flow stores the local verification status of the current session.
-     *
-     * We should ideally base the verified status in the Rust SDK info, but there are several issues with that approach:
-     *
-     * - The SDK takes a while to report this value, resulting in a delay of 1-2s in displaying the UI.
-     * - We need to add a 'Skip' option for testing purposes, which would not be possible if we relied only on the SDK.
-     * - The SDK sometimes doesn't report the verification state if there is no network connection when the app boots.
-     */
-    val needsVerificationFlow: StateFlow<Boolean>
-
     /**
      * State of the current verification flow ([VerificationFlowState.Initial] if not started).
      */
@@ -83,11 +72,6 @@ interface SessionVerificationService {
      * Returns the verification service state to the initial step.
      */
     suspend fun reset()
-
-    /**
-     * Saves the current session state as [verified].
-     */
-    suspend fun saveVerifiedState(verified: Boolean)
 }

 /** Verification status of the current session. */
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index a24c4a35bbc..91036df1cc4 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -160,7 +160,6 @@ class RustMatrixClient(
         syncService = rustSyncService,
         sessionCoroutineScope = sessionCoroutineScope,
         dispatchers = dispatchers,
-        sessionStore = sessionStore,
     )

     private val roomDirectoryService = RustRoomDirectoryService(
@@ -188,7 +187,6 @@ class RustMatrixClient(
                             isTokenValid = false,
                             loginType = existingData.loginType,
                             passphrase = existingData.passphrase,
-                            needsVerification = existingData.needsVerification,
                         )
                         sessionStore.updateData(newData)
                         Timber.d("Removed session data with token: '...$anonymizedToken'.")
@@ -216,7 +214,6 @@ class RustMatrixClient(
                     isTokenValid = true,
                     loginType = existingData.loginType,
                     passphrase = existingData.passphrase,
-                    needsVerification = existingData.needsVerification,
                 )
                 sessionStore.updateData(newData)
                 Timber.d("Saved new session data with token: '...$anonymizedToken'.")
@@ -242,7 +239,6 @@ class RustMatrixClient(
         client = client,
         isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
         sessionCoroutineScope = sessionCoroutineScope,
-        sessionStore = sessionStore,
     )

     private val eventFilters = TimelineConfig.excludedEvents
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index 0ac0ce94f36..50d82c1e3ab 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -138,7 +138,6 @@ class RustMatrixAuthenticationService @Inject constructor(
                         isTokenValid = true,
                         loginType = LoginType.PASSWORD,
                         passphrase = pendingPassphrase,
-                        needsVerification = true,
                     )
                 }
                 sessionStore.storeData(sessionData)
@@ -187,7 +186,6 @@ class RustMatrixAuthenticationService @Inject constructor(
                         isTokenValid = true,
                         loginType = LoginType.OIDC,
                         passphrase = pendingPassphrase,
-                        needsVerification = true,
                     )
                 }
                 pendingOidcAuthenticationData?.close()
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
index 25881c21741..f5a63909893 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
@@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
 import io.element.android.libraries.matrix.api.encryption.RecoveryState
 import io.element.android.libraries.matrix.api.sync.SyncState
 import io.element.android.libraries.matrix.impl.sync.RustSyncService
-import io.element.android.libraries.sessionstorage.api.SessionStore
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.currentCoroutineContext
@@ -49,11 +48,10 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover
 import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException

 internal class RustEncryptionService(
-    private val client: Client,
+    client: Client,
     syncService: RustSyncService,
     sessionCoroutineScope: CoroutineScope,
     private val dispatchers: CoroutineDispatchers,
-    private val sessionStore: SessionStore,
 ) : EncryptionService {
     private val service: Encryption = client.encryption()

@@ -188,9 +186,6 @@ internal class RustEncryptionService(
     override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
         runCatching {
             service.recover(recoveryKey)
-            val existingSession = sessionStore.getSession(client.userId())
-                ?: error("Failed to save verification state. No session with id ${client.userId()}")
-            sessionStore.updateData(existingSession.copy(needsVerification = false))
         }.mapFailure {
             it.mapRecoveryException()
         }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
index 3c1e3c40ec9..aea838b7051 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt
@@ -25,7 +25,6 @@ internal fun Session.toSessionData(
     isTokenValid: Boolean,
     loginType: LoginType,
     passphrase: String?,
-    needsVerification: Boolean,
 ) = SessionData(
     userId = userId,
     deviceId = deviceId,
@@ -38,5 +37,4 @@ internal fun Session.toSessionData(
     isTokenValid = isTokenValid,
     loginType = loginType,
     passphrase = passphrase,
-    needsVerification = needsVerification,
 )
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
index 3d2fe0cc403..88403e89285 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
@@ -16,7 +16,6 @@

 package io.element.android.libraries.matrix.impl.verification

-import io.element.android.libraries.core.bool.orFalse
 import io.element.android.libraries.core.data.tryOrNull
 import io.element.android.libraries.matrix.api.verification.SessionVerificationData
 import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -24,7 +23,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
 import io.element.android.libraries.matrix.api.verification.VerificationEmoji
 import io.element.android.libraries.matrix.api.verification.VerificationFlowState
 import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
-import io.element.android.libraries.sessionstorage.api.SessionStore
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
@@ -33,10 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -58,7 +53,6 @@ class RustSessionVerificationService(
     private val client: Client,
     isSyncServiceReady: Flow<Boolean>,
     private val sessionCoroutineScope: CoroutineScope,
-    private val sessionStore: SessionStore,
 ) : SessionVerificationService, SessionVerificationControllerDelegate {
     private val encryptionService: Encryption = client.encryption()
     private lateinit var verificationController: SessionVerificationController
@@ -80,11 +74,6 @@ class RustSessionVerificationService(
         }
     })

-    override val needsVerificationFlow: StateFlow<Boolean> = sessionStore.sessionsFlow()
-        .map { sessions -> sessions.firstOrNull { it.userId == client.userId() }?.needsVerification.orFalse() }
-        .distinctUntilChanged()
-        .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
-
     private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
     override val verificationFlowState = _verificationFlowState.asStateFlow()

@@ -98,6 +87,9 @@ class RustSessionVerificationService(
     }

     init {
+        // Update initial state in case sliding sync isn't ready
+        updateVerificationStatus(encryptionService.verificationState())
+
         isReady.onEach { isReady ->
             if (isReady) {
                 Timber.d("Starting verification service")
@@ -165,7 +157,6 @@ class RustSessionVerificationService(
                 }
             }
                 .onSuccess {
-                    saveVerifiedState(true)
                     updateVerificationStatus(VerificationState.VERIFIED)
                     _verificationFlowState.value = VerificationFlowState.Finished
                 }
@@ -195,14 +186,6 @@ class RustSessionVerificationService(
         _verificationFlowState.value = VerificationFlowState.Initial
     }

-    override suspend fun saveVerifiedState(verified: Boolean) = tryOrFail {
-        val existingSession = sessionStore.getSession(client.userId())
-            ?: error("Failed to save verification state. No session with id ${client.userId()}")
-        sessionStore.updateData(existingSession.copy(needsVerification = !verified))
-        // Wait until the new state is saved
-        needsVerificationFlow.first { needsVerification -> !needsVerification }
-    }
-
     fun destroy() {
         Timber.d("Destroying RustSessionVerificationService")
         verificationStateListenerTaskHandle.cancelAndDestroy()
diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt
index 948e9acb75e..9bbeadc7091 100644
--- a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt
+++ b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt
@@ -34,5 +34,8 @@ interface SessionPreferencesStore {
     suspend fun setRenderTypingNotifications(enabled: Boolean)
     fun isRenderTypingNotificationsEnabled(): Flow<Boolean>

+    suspend fun setSkipSessionVerification(skip: Boolean)
+    fun isSessionVerificationSkipped(): Flow<Boolean>
+
     suspend fun clear()
 }
diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
index 05bbe9c048a..41b52d87f0a 100644
--- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
+++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
@@ -49,6 +49,7 @@ class DefaultSessionPreferencesStore(
     private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
     private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
     private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
+    private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")

     private val dataStoreFile = storeFile(context, sessionId)
     private val store = PreferenceDataStoreFactory.create(
@@ -86,6 +87,9 @@ class DefaultSessionPreferencesStore(
     override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
     override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }

+    override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
+    override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
+
     override suspend fun clear() {
         dataStoreFile.safeDelete()
     }
diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt
index dffc1d1bbb9..76bd7f743a7 100644
--- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt
+++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt
@@ -44,6 +44,4 @@ data class SessionData(
     val loginType: LoginType,
     /** The optional passphrase used to encrypt data in the SDK local store. */
     val passphrase: String?,
-    /** Whether the session needs verification. */
-    val needsVerification: Boolean,
 )
diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt
index 6b79e867d0e..3824def48c0 100644
--- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt
+++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt
@@ -34,7 +34,6 @@ internal fun SessionData.toDbModel(): DbSessionData {
         isTokenValid = if (isTokenValid) 1L else 0L,
         loginType = loginType.name,
         passphrase = passphrase,
-        needsVerification = if (needsVerification) 1L else 0L,
     )
 }

@@ -51,6 +50,5 @@ internal fun DbSessionData.toApiModel(): SessionData {
         isTokenValid = isTokenValid == 1L,
         loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
         passphrase = passphrase,
-        needsVerification = needsVerification == 1L,
     )
 }
diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/7.db b/libraries/session-storage/impl/src/main/sqldelight/databases/7.db
new file mode 100644
index 00000000000..61805e1317f
Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/7.db differ
diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
index 56b036ec966..c33b4d7c7ec 100644
--- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
+++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
@@ -23,9 +23,7 @@ CREATE TABLE SessionData (
     isTokenValid INTEGER NOT NULL DEFAULT 1,
     loginType TEXT,
     -- added in version 5
-    passphrase TEXT,
-    -- added in version 6
-    needsVerification INTEGER NOT NULL DEFAULT 0
+    passphrase TEXT
 );

diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/6.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/6.sqm
new file mode 100644
index 00000000000..e4eccfdb8da
--- /dev/null
+++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/6.sqm
@@ -0,0 +1,20 @@
+-- Migrate DB from version 6
+-- Remove DB value for verified status, we're back to using the Rust SDK as a source of truth
+
+CREATE TABLE SessionData_bak (
+    userId TEXT NOT NULL PRIMARY KEY,
+    deviceId TEXT NOT NULL,
+    accessToken TEXT NOT NULL,
+    refreshToken TEXT,
+    homeserverUrl TEXT NOT NULL,
+    slidingSyncProxy TEXT,
+    loginTimestamp INTEGER,
+    oidcData TEXT,
+    isTokenValid INTEGER NOT NULL DEFAULT 1,
+    loginType TEXT,
+    passphrase TEXT
+);
+
+INSERT INTO SessionData_bak SELECT userId, deviceId, accessToken, refreshToken, homeserverUrl, slidingSyncProxy, loginTimestamp, oidcData, isTokenValid, loginType, passphrase FROM SessionData;
+DROP TABLE SessionData;
+ALTER TABLE SessionData_bak RENAME TO SessionData;

diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt
index d381d945a92..9cc3db8d295 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt
@@ -24,6 +24,7 @@ import io.element.android.features.ftue.impl.state.DefaultFtueService
 import io.element.android.features.ftue.impl.state.FtueStep
 import io.element.android.features.lockscreen.api.LockScreenService
 import io.element.android.features.lockscreen.test.FakeLockScreenService
+import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
 import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
 import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
 import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
@@ -90,7 +91,6 @@ class DefaultFtueServiceTests {
     fun `traverse flow`() = runTest {
         val sessionVerificationService = FakeSessionVerificationService().apply {
             givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
-            givenNeedsVerification(true)
         }
         val analyticsService = FakeAnalyticsService()
         val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
@@ -108,7 +108,7 @@ class DefaultFtueServiceTests {

         // Session verification
         steps.add(state.getNextStep(steps.lastOrNull()))
-        sessionVerificationService.givenNeedsVerification(false)
+        sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)

         // Notifications opt in
         steps.add(state.getNextStep(steps.lastOrNull()))
@@ -200,6 +200,7 @@ class DefaultFtueServiceTests {
         analyticsService: AnalyticsService = FakeAnalyticsService(),
         permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
         lockScreenService: LockScreenService = FakeLockScreenService(),
+        sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
         // First version where notification permission is required
         sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
     ) = DefaultFtueService(
@@ -209,5 +210,6 @@ class DefaultFtueServiceTests {
         analyticsService = analyticsService,
         permissionStateProvider = permissionStateProvider,
         lockScreenService = lockScreenService,
+        sessionPreferencesStore = sessionPreferencesStore,
     )
 }
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt
index 06f69e16284..9100cc65c9b 100644
--- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt
@@ -24,6 +24,7 @@ import com.google.common.truth.Truth.assertThat
 import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
 import io.element.android.libraries.architecture.AsyncData
 import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
 import io.element.android.libraries.matrix.api.encryption.EncryptionService
 import io.element.android.libraries.matrix.api.encryption.RecoveryState
 import io.element.android.libraries.matrix.api.verification.SessionVerificationData
@@ -35,7 +36,6 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
 import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
 import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
 import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.lambda.value
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
 import org.junit.Rule
@@ -289,7 +289,6 @@ class VerifySelfSessionPresenterTests {
         }.test {
             val state = requestVerificationAndAwaitVerifyingState(service)
             state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
-            service.saveVerifiedStateResult.assertions().isCalledOnce().with(value(true))
             assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
         }
     }
@@ -297,12 +296,16 @@ class VerifySelfSessionPresenterTests {
     @Test
     fun `present - When verification is not needed, the flow is completed`() = runTest {
         val service = FakeSessionVerificationService().apply {
-            givenNeedsVerification(false)
+            givenCanVerifySession(false)
+            givenIsReady(true)
+            givenVerifiedStatus(SessionVerifiedStatus.Verified)
+            givenVerificationFlowState(VerificationFlowState.Finished)
         }
         val presenter = createVerifySelfSessionPresenter(service)
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
+            skipItems(1)
             assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
         }
     }
@@ -334,7 +337,6 @@ class VerifySelfSessionPresenterTests {
     private fun unverifiedSessionService(): FakeSessionVerificationService {
         return FakeSessionVerificationService().apply {
             givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
-            givenNeedsVerification(true)
         }
     }

@@ -342,12 +344,14 @@ class VerifySelfSessionPresenterTests {
         service: SessionVerificationService = unverifiedSessionService(),
         encryptionService: EncryptionService = FakeEncryptionService(),
         buildMeta: BuildMeta = aBuildMeta(),
+        sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
     ): VerifySelfSessionPresenter {
         return VerifySelfSessionPresenter(
             sessionVerificationService = service,
             encryptionService = encryptionService,
             stateMachine = VerifySelfSessionStateMachine(service, encryptionService),
             buildMeta = buildMeta,
+            sessionPreferencesStore = sessionPreferencesStore,
         )
     }
 }
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
index 78239102632..64c7c8ab974 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
@@ -20,22 +20,17 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
 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.VerificationFlowState
-import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
-import io.element.android.tests.testutils.lambda.lambdaRecorder
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow

-class FakeSessionVerificationService(
-    var saveVerifiedStateResult: LambdaOneParamRecorder<Boolean, Unit> = lambdaRecorder<Boolean, Unit> {}
-) : SessionVerificationService {
+class FakeSessionVerificationService : SessionVerificationService {
     private val _isReady = MutableStateFlow(false)
     private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
     private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
     private var _canVerifySessionFlow = MutableStateFlow(true)
     var shouldFail = false

-    override val needsVerificationFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
     override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
     override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
@@ -94,15 +89,7 @@ class FakeSessionVerificationService(
         _isReady.value = value
     }

-    fun givenNeedsVerification(value: Boolean) {
-        needsVerificationFlow.value = value
-    }
-
     override suspend fun reset() {
         _verificationFlowState.value = VerificationFlowState.Initial
     }
-
-    override suspend fun saveVerifiedState(verified: Boolean) {
-        saveVerifiedStateResult(verified)
-    }
 }
diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt
index 9e9fc7ba2ab..16f98d08705 100644
--- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt
+++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt
@@ -26,12 +26,14 @@ class InMemorySessionPreferencesStore(
     isRenderReadReceiptsEnabled: Boolean = true,
     isSendTypingNotificationsEnabled: Boolean = true,
     isRenderTypingNotificationsEnabled: Boolean = true,
+    isSessionVerificationSkipped: Boolean = false,
 ) : SessionPreferencesStore {
     private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
     private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
     private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
     private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
     private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
+    private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
     var clearCallCount = 0
         private set

@@ -65,6 +67,14 @@ class InMemorySessionPreferencesStore(

     override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled

+    override suspend fun setSkipSessionVerification(skip: Boolean) {
+        isSessionVerificationSkipped.tryEmit(skip)
+    }
+
+    override fun isSessionVerificationSkipped(): Flow<Boolean> {
+        return isSessionVerificationSkipped
+    }
+
     override suspend fun clear() {
         clearCallCount++
         isSendPublicReadReceiptsEnabled.tryEmit(true)
diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt
index df35ec59444..46e90f6d524 100644
--- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt
+++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt
@@ -144,7 +144,6 @@ class DatabaseSessionStoreTests {
             isTokenValid = 1,
             loginType = null,
             passphrase = "aPassphrase",
-            needsVerification = 1L,
         )
         val secondSessionData = SessionData(
             userId = "userId",
@@ -158,7 +157,6 @@ class DatabaseSessionStoreTests {
             isTokenValid = 1,
             loginType = null,
             passphrase = "aPassphraseAltered",
-            needsVerification = 0L,
         )
         assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
         assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
@@ -179,7 +177,6 @@ class DatabaseSessionStoreTests {
         assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
         assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
         assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase)
-        assertThat(alteredSession.needsVerification).isEqualTo(secondSessionData.needsVerification)
     }

     @Test
@@ -196,7 +193,6 @@ class DatabaseSessionStoreTests {
             isTokenValid = 1,
             loginType = null,
             passphrase = "aPassphrase",
-            needsVerification = 1L,
         )
         val secondSessionData = SessionData(
             userId = "userIdUnknown",
@@ -210,7 +206,6 @@ class DatabaseSessionStoreTests {
             isTokenValid = 1,
             loginType = null,
             passphrase = "aPassphraseAltered",
-            needsVerification = 0L,
         )
         assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)

@@ -229,6 +224,5 @@ class DatabaseSessionStoreTests {
         assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
         assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData)
         assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase)
-        assertThat(notAlteredSession.needsVerification).isEqualTo(firstSessionData.needsVerification)
     }
 }
diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt
index 1397e260b93..341e5e0e923 100644
--- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt
+++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt
@@ -31,5 +31,4 @@ internal fun aSessionData() = SessionData(
     isTokenValid = 1,
     loginType = LoginType.UNKNOWN.name,
     passphrase = null,
-    needsVerification = 0L,
 )


Mark room as fully read when user goes back to the room list.

<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Type of change

- [ ] Feature
- [x] Bugfix
- [ ] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Send `m.fully_read` read marker once the user is going back to the room list.
Second commit is just removing noise in the logs.

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #2281

## Screenshots / GIFs

<!--
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.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- See #2281, hence, I did not manage to see any "New" line on my messages during the tests...

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] 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
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] 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)
- [ ] You've made a self review of your PR

Own read marker (new) does not get updated when reading the room
### Steps to reproduce

1. Open a room with unread messages
2. Observe the "New" marker being at the correct place
3. Scroll and read through the room as long as you like
4. Close the room and open it again
5. Observe the "New" marker being at the same place as before instead of vanishing/updating to the latest message
6. (Bonus: Open the room with Element-Android or Element-Web and notice the "New" marker being updated in Element-X)

### Outcome

#### What did you expect?
"New" marker to be updated when opening the room

#### What happened instead?
Nothing. The "New" marker stays at the same place unless one opens the room with another client. This makes it very hard to keep up with a semi high-traffic room using Element-X

### Your phone model

Oneplus 6

### Operating system version

Android 14

### Application version and app store

Element-X 0.4.2 (and up to date nightly)

### Homeserver

Synapse 1.99.0

### Will you send logs?

No

### Are you willing to provide a PR?

No

I believe Element X simply does not update the unread marker at all, atleast the chat room was marked read in Element-Web, but the unread marker was not updated (showing unread messages farther up the room)
We need send the receipt `FULLY_READ` when the user goes back to the room list.

diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt
new file mode 100644
index 00000000000..b2962e2f5ad
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/MarkAsFullyRead.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.timeline.ReceiptType
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+interface MarkAsFullyRead {
+    operator fun invoke(roomId: RoomId)
+}
+
+@ContributesBinding(SessionScope::class)
+class DefaultMarkAsFullyRead @Inject constructor(
+    private val matrixClient: MatrixClient,
+) : MarkAsFullyRead {
+    override fun invoke(roomId: RoomId) {
+        matrixClient.sessionCoroutineScope.launch {
+            matrixClient.getRoom(roomId)?.use { room ->
+                room.markAsRead(receiptType = ReceiptType.FULLY_READ)
+                    .onFailure {
+                        Timber.e("Failed to mark room $roomId as fully read", it)
+                    }
+            }
+        }
+    }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index 3be27fe7d0a..2ca61fc8fee 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
@@ -8,6 +8,7 @@
 package io.element.android.features.messages.impl.timeline

 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.collectAsState
@@ -76,6 +77,7 @@ class TimelinePresenter @AssistedInject constructor(
     private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
     private val typingNotificationPresenter: Presenter<TypingNotificationState>,
     private val roomCallStatePresenter: Presenter<RoomCallState>,
+    private val markAsFullyRead: MarkAsFullyRead,
 ) : Presenter<TimelineState> {
     @AssistedFactory
     interface Factory {
@@ -177,6 +179,12 @@ class TimelinePresenter @AssistedInject constructor(
             }
         }

+        DisposableEffect(Unit) {
+            onDispose {
+                markAsFullyRead(room.roomId)
+            }
+        }
+
         LaunchedEffect(Unit) {
             timelineItemsFactory.timelineItems
                 .onEach { newTimelineItems ->
diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt
index 7e7fc51d71a..a64d2733791 100644
--- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt
+++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt
@@ -48,7 +48,6 @@ class DefaultEncoder @Inject constructor(

     override fun release() {
         encoder?.release()
-            ?: Timber.w("Can't release encoder that is not initialized")
         encoder = null
     }
 }

diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
index f45dbe214c1..e42941da0d8 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
@@ -139,10 +139,10 @@ class MessagesPresenterTest {
                 canRedactOtherResult = { Result.success(true) },
                 canUserJoinCallResult = { Result.success(true) },
                 canUserPinUnpinResult = { Result.success(true) },
+                markAsReadResult = { lambdaError() }
             ),
             typingNoticeResult = { Result.success(Unit) },
         )
-        assertThat(room.baseRoom.markAsReadCalls).isEmpty()
         val presenter = createMessagesPresenter(joinedRoom = room)
         presenter.testWithLifecycleOwner {
             runCurrent()
@@ -848,12 +848,12 @@ class MessagesPresenterTest {
     fun `present - permission to redact other`() = runTest {
         val joinedRoom = FakeJoinedRoom(
             baseRoom = FakeBaseRoom(
-            canRedactOtherResult = { Result.success(true) },
-            canUserSendMessageResult = { _, _ -> Result.success(true) },
-            canRedactOwnResult = { Result.success(false) },
-            canUserJoinCallResult = { Result.success(true) },
-            canUserPinUnpinResult = { Result.success(true) },
-        ),
+                canRedactOtherResult = { Result.success(true) },
+                canUserSendMessageResult = { _, _ -> Result.success(true) },
+                canRedactOwnResult = { Result.success(false) },
+                canUserJoinCallResult = { Result.success(true) },
+                canUserPinUnpinResult = { Result.success(true) },
+            ),
             typingNoticeResult = { Result.success(Unit) },
         )
         val presenter = createMessagesPresenter(joinedRoom = joinedRoom)
@@ -897,12 +897,12 @@ class MessagesPresenterTest {
         val timeline = FakeTimeline()
         val room = FakeJoinedRoom(
             baseRoom = FakeBaseRoom(
-            canUserSendMessageResult = { _, _ -> Result.success(true) },
-            canRedactOwnResult = { Result.success(true) },
-            canRedactOtherResult = { Result.success(true) },
-            canUserJoinCallResult = { Result.success(true) },
-            canUserPinUnpinResult = { Result.success(true) },
-        ),
+                canUserSendMessageResult = { _, _ -> Result.success(true) },
+                canRedactOwnResult = { Result.success(true) },
+                canRedactOtherResult = { Result.success(true) },
+                canUserJoinCallResult = { Result.success(true) },
+                canUserPinUnpinResult = { Result.success(true) },
+            ),
             liveTimeline = timeline,
             typingNoticeResult = { Result.success(Unit) },
         )
@@ -937,12 +937,12 @@ class MessagesPresenterTest {
         val analyticsService = FakeAnalyticsService()
         val room = FakeJoinedRoom(
             baseRoom = FakeBaseRoom(
-            canUserSendMessageResult = { _, _ -> Result.success(true) },
-            canRedactOwnResult = { Result.success(true) },
-            canRedactOtherResult = { Result.success(true) },
-            canUserJoinCallResult = { Result.success(true) },
-            canUserPinUnpinResult = { Result.success(true) },
-        ),
+                canUserSendMessageResult = { _, _ -> Result.success(true) },
+                canRedactOwnResult = { Result.success(true) },
+                canRedactOtherResult = { Result.success(true) },
+                canUserJoinCallResult = { Result.success(true) },
+                canUserPinUnpinResult = { Result.success(true) },
+            ),
             liveTimeline = timeline,
             typingNoticeResult = { Result.success(Unit) },
         )
@@ -1096,12 +1096,12 @@ class MessagesPresenterTest {
         }
         val room = FakeJoinedRoom(
             baseRoom = FakeBaseRoom(
-            canUserSendMessageResult = { _, _ -> Result.success(true) },
-            canRedactOwnResult = { Result.success(true) },
-            canRedactOtherResult = { Result.success(true) },
-            canUserJoinCallResult = { Result.success(true) },
-            canUserPinUnpinResult = { Result.success(true) },
-        ),
+                canUserSendMessageResult = { _, _ -> Result.success(true) },
+                canRedactOwnResult = { Result.success(true) },
+                canRedactOtherResult = { Result.success(true) },
+                canUserJoinCallResult = { Result.success(true) },
+                canUserPinUnpinResult = { Result.success(true) },
+            ),
             liveTimeline = timeline,
             typingNoticeResult = { Result.success(Unit) },
         )
@@ -1134,16 +1134,16 @@ class MessagesPresenterTest {
     fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest {
         val room = FakeJoinedRoom(
             baseRoom = FakeBaseRoom(
-            sessionId = A_SESSION_ID,
-            canUserSendMessageResult = { _, _ -> Result.success(true) },
-            canRedactOwnResult = { Result.success(true) },
-            canRedactOtherResult = { Result.success(true) },
-            canUserJoinCallResult = { Result.success(true) },
-            canUserPinUnpinResult = { Result.success(true) },
-            initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true)
-        ).apply {
-            givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
-        },
+                sessionId = A_SESSION_ID,
+                canUserSendMessageResult = { _, _ -> Result.success(true) },
+                canRedactOwnResult = { Result.success(true) },
+                canRedactOtherResult = { Result.success(true) },
+                canUserJoinCallResult = { Result.success(true) },
+                canUserPinUnpinResult = { Result.success(true) },
+                initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true)
+            ).apply {
+                givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
+            },
             typingNoticeResult = { Result.success(Unit) },
         )
         val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) })
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt
new file mode 100644
index 00000000000..6340ce56a5c
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultMarkAsFullyReadTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.features.messages.impl.timeline
+
+import io.element.android.libraries.matrix.api.timeline.ReceiptType
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.room.FakeBaseRoom
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class DefaultMarkAsFullyReadTest {
+    @Test
+    fun `When room is not found, then no exception is thrown`() = runTest {
+        val markAsFullyRead = DefaultMarkAsFullyRead(
+            FakeMatrixClient(
+                sessionCoroutineScope = backgroundScope,
+            ).apply {
+                givenGetRoomResult(A_ROOM_ID, null)
+            }
+        )
+        markAsFullyRead.invoke(A_ROOM_ID)
+        runCurrent()
+    }
+
+    @Test
+    fun `When room is found, the expected method is invoked`() = runTest {
+        val markAsReadResult = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
+        val baseRoom = FakeBaseRoom(
+            markAsReadResult = markAsReadResult
+        )
+        val markAsFullyRead = DefaultMarkAsFullyRead(
+            FakeMatrixClient(
+                sessionCoroutineScope = backgroundScope,
+            ).apply {
+                givenGetRoomResult(A_ROOM_ID, baseRoom)
+            }
+        )
+        markAsFullyRead.invoke(A_ROOM_ID)
+        runCurrent()
+        markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.FULLY_READ))
+        baseRoom.assertDestroyed()
+    }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt
new file mode 100644
index 00000000000..895676a1260
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/FakeMarkAsFullyRead.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeMarkAsFullyRead(
+    private val invokeResult: (RoomId) -> Unit = { lambdaError() }
+) : MarkAsFullyRead {
+    override fun invoke(roomId: RoomId) {
+        invokeResult(roomId)
+    }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
index 9cbce38709f..32b48f8a82b 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
@@ -29,6 +29,7 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction
 import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
 import io.element.android.features.roomcall.api.aStandByCallState
 import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
 import io.element.android.libraries.matrix.api.core.UniqueId
 import io.element.android.libraries.matrix.api.room.RoomMembersState
 import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
@@ -40,6 +41,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
 import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
 import io.element.android.libraries.matrix.test.AN_EVENT_ID
 import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
+import io.element.android.libraries.matrix.test.A_ROOM_ID
 import io.element.android.libraries.matrix.test.A_UNIQUE_ID
 import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
 import io.element.android.libraries.matrix.test.A_USER_ID
@@ -58,6 +60,7 @@ import io.element.android.tests.testutils.lambda.any
 import io.element.android.tests.testutils.lambda.assert
 import io.element.android.tests.testutils.lambda.lambdaRecorder
 import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
 import io.element.android.tests.testutils.testCoroutineDispatchers
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -76,7 +79,9 @@ import java.util.Date
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.seconds

-@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
+@Suppress("LargeClass")
+@OptIn(ExperimentalCoroutinesApi::class)
+class TimelinePresenterTest {
     @get:Rule
     val warmUpRule = WarmUpRule()

@@ -119,9 +124,26 @@ import kotlin.time.Duration.Companion.seconds
         }
     }

-    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
-    fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
+    fun `present - on scroll finished mark a room as read if the first visible index is 0 - read private`() {
+        `present - on scroll finished mark a room as read if the first visible index is 0`(
+            isSendPublicReadReceiptsEnabled = false,
+            expectedReceiptType = ReceiptType.READ_PRIVATE,
+        )
+    }
+
+    @Test
+    fun `present - on scroll finished mark a room as read if the first visible index is 0 - read`() {
+        `present - on scroll finished mark a room as read if the first visible index is 0`(
+            isSendPublicReadReceiptsEnabled = true,
+            expectedReceiptType = ReceiptType.READ,
+        )
+    }
+
+    private fun `present - on scroll finished mark a room as read if the first visible index is 0`(
+        isSendPublicReadReceiptsEnabled: Boolean,
+        expectedReceiptType: ReceiptType,
+    ) = runTest(StandardTestDispatcher()) {
         val timeline = FakeTimeline(
             timelineItems = flowOf(
                 listOf(
@@ -129,11 +151,15 @@ import kotlin.time.Duration.Companion.seconds
                 )
             )
         )
+        val markAsReadResult = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
         val room = FakeJoinedRoom(
             liveTimeline = timeline,
-            baseRoom = FakeBaseRoom(canUserSendMessageResult = { _, _ -> Result.success(true) })
+            baseRoom = FakeBaseRoom(
+                canUserSendMessageResult = { _, _ -> Result.success(true) },
+                markAsReadResult = markAsReadResult,
+            )
         )
-        val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
+        val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled)
         val presenter = createTimelinePresenter(
             timeline = timeline,
             room = room,
@@ -145,11 +171,32 @@ import kotlin.time.Duration.Companion.seconds
             val initialState = awaitFirstItem()
             initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
             runCurrent()
-            assertThat(room.baseRoom.markAsReadCalls).isNotEmpty()
+            assert(markAsReadResult)
+                .isCalledOnce()
+                .with(value(expectedReceiptType))
             cancelAndIgnoreRemainingEvents()
         }
     }

+    @Test
+    fun `present - once presenter is disposed, room is marked as fully read`() = runTest {
+        val invokeResult = lambdaRecorder<RoomId, Unit> { }
+        val presenter = createTimelinePresenter(
+            room = FakeJoinedRoom(
+                baseRoom = FakeBaseRoom(
+                    canUserSendMessageResult = { _, _ -> Result.success(true) },
+                )
+            ),
+            markAsFullyRead = FakeMarkAsFullyRead(
+                invokeResult = invokeResult,
+            )
+        )
+        presenter.test {
+            awaitFirstItem()
+        }
+        invokeResult.assertions().isCalledOnce().with(value(A_ROOM_ID))
+    }
+
     @Test
     fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
         val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
@@ -674,6 +721,7 @@ import kotlin.time.Duration.Companion.seconds
         sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
         sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
         timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
+        markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(),
     ): TimelinePresenter {
         return TimelinePresenter(
             timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
@@ -690,6 +738,7 @@ import kotlin.time.Duration.Companion.seconds
             resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
             typingNotificationPresenter = { aTypingNotificationState() },
             roomCallStatePresenter = { aStandByCallState() },
+            markAsFullyRead = markAsFullyRead,
         )
     }
 }
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
index 5726eb1eb0b..6edc2bd5bd8 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
@@ -504,9 +504,18 @@ class RoomListPresenterTest {

     @Test
     fun `present - check that the room is marked as read with correct RR and as unread`() = runTest {
-        val room = FakeBaseRoom()
-        val room2 = FakeBaseRoom(roomId = A_ROOM_ID_2)
-        val room3 = FakeBaseRoom(roomId = A_ROOM_ID_3)
+        val markAsReadResult = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
+        val markAsReadResult3 = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
+        val room = FakeBaseRoom(
+            markAsReadResult = markAsReadResult,
+        )
+        val room2 = FakeBaseRoom(
+            roomId = A_ROOM_ID_2,
+        )
+        val room3 = FakeBaseRoom(
+            roomId = A_ROOM_ID_3,
+            markAsReadResult = markAsReadResult3,
+        )
         val allRooms = setOf(room, room2, room3)
         val sessionPreferencesStore = InMemorySessionPreferencesStore()
         val matrixClient = FakeMatrixClient().apply {
@@ -530,21 +539,19 @@ class RoomListPresenterTest {
         }.test {
             val initialState = awaitItem()
             allRooms.forEach {
-                assertThat(it.markAsReadCalls).isEmpty()
                 assertThat(it.setUnreadFlagCalls).isEmpty()
             }
             initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
-            assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
+            markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ))
             assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
             clearMessagesForRoomLambda.assertions().isCalledOnce()
                 .with(value(A_SESSION_ID), value(A_ROOM_ID))
             initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID_2))
-            assertThat(room2.markAsReadCalls).isEmpty()
             assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true))
             // Test again with private read receipts
             sessionPreferencesStore.setSendPublicReadReceipts(false)
             initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID_3))
-            assertThat(room3.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ_PRIVATE))
+            markAsReadResult3.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE))
             assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false))
             clearMessagesForRoomLambda.assertions().isCalledExactly(2)
                 .withSequence(
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt
index d755722bd39..ae9aee87361 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt
@@ -54,6 +54,7 @@ class FakeBaseRoom(
     private val canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() },
     private val canUserPinUnpinResult: (UserId) -> Result<Boolean> = { lambdaError() },
     private val setIsFavoriteResult: (Boolean) -> Result<Unit> = { lambdaError() },
+    private val markAsReadResult: (ReceiptType) -> Result<Unit> = { Result.success(Unit) },
     private val powerLevelsResult: () -> Result<RoomPowerLevels> = { lambdaError() },
     private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
     private val updateMembersResult: () -> Unit = { lambdaError() },
@@ -183,11 +184,8 @@ class FakeBaseRoom(
         return setIsFavoriteResult(isFavorite)
     }

-    val markAsReadCalls = mutableListOf<ReceiptType>()
-
     override suspend fun markAsRead(receiptType: ReceiptType): Result<Unit> {
-        markAsReadCalls.add(receiptType)
-        return Result.success(Unit)
+        return markAsReadResult(receiptType)
     }

     var setUnreadFlagCalls = mutableListOf<Boolean>()
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt
index ac53bace79d..c51e7ad7a85 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt
@@ -8,7 +8,6 @@
 package io.element.android.libraries.push.impl.notifications

 import android.content.Intent
-import com.google.common.truth.Truth.assertThat
 import io.element.android.libraries.matrix.api.MatrixClient
 import io.element.android.libraries.matrix.api.core.EventId
 import io.element.android.libraries.matrix.api.core.RoomId
@@ -224,7 +223,12 @@ class NotificationBroadcastReceiverHandlerTest {
             getLambda = getLambda
         )
         val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
-        val joinedRoom = FakeJoinedRoom()
+        val markAsReadResult = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
+        val joinedRoom = FakeJoinedRoom(
+            baseRoom = FakeBaseRoom(
+                markAsReadResult = markAsReadResult,
+            ),
+        )
         val fakeNotificationCleaner = FakeNotificationCleaner(
             clearMessagesForRoomLambda = clearMessagesForRoomLambda,
         )
@@ -243,7 +247,7 @@ class NotificationBroadcastReceiverHandlerTest {
         clearMessagesForRoomLambda.assertions()
             .isCalledOnce()
             .with(value(A_SESSION_ID), value(A_ROOM_ID))
-        assertThat(joinedRoom.baseRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType))
+        markAsReadResult.assertions().isCalledOnce().with(value(expectedReceiptType))
     }

     @Test