Add banner for optional migration to simplified sliding sync

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

## Content

- Add `MatrixClient.isNativeSlidingSyncSupported()` and `MatrixClient.isUsingNativeSlidingSync` to check whether the home server supports native sliding sync and we're already using it.
- Add `NativeSlidingSyncMigrationBanner` composable to the `RoomList` screen when the home server supports native sliding sync but the current session is not using it.
- Add an extra logout successful action to the logout flow, create `EnableNativeSlidingSyncUseCase` so it can be used there.

## Motivation and context

Closes #3427.

## Screenshots / GIFs

In the PR.

## Tests

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

- With simplified sliding sync disabled in the client, log into a homeserver which has it enabled (i.e. element.io).
- You should see the migration banner.
- When tapping in log out & upgrade, a confirmation will be displayed.
- Once logged out if you log in again you'll be using SSS.
- The banner won't be visible then.

## Tested devices

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

## 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 title will be used in the release note, it clearly define what will change for the user
- [x] 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

Offer user to transition to SSS (Android)
1. Follow the logic defined in the parent issue to prompt the user to log out to complete the migration to SSS.
2. For prompt choose platform-native implementation (whatever is the easiest to implement).

@amshakal Would you explicitly create the designs in Figma in such a case? I think it would be preferred to make it more specific.
Designs for Android: https://www.figma.com/design/LwZ8S0VbzKqekK1akuoh8z/Launch-%2B-Onboarding-(new)?node-id=241-4337&t=Y89GDbdGbVjhMV83-1

diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index db153314d62..5a8db185ef3 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -40,6 +40,7 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
 import io.element.android.features.ftue.api.FtueEntryPoint
 import io.element.android.features.ftue.api.state.FtueService
 import io.element.android.features.ftue.api.state.FtueState
+import io.element.android.features.logout.api.LogoutEntryPoint
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.api.NetworkStatus
 import io.element.android.features.preferences.api.PreferencesEntryPoint
@@ -65,6 +66,7 @@ import io.element.android.libraries.matrix.api.core.UserId
 import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
 import io.element.android.libraries.matrix.api.permalink.PermalinkData
 import io.element.android.libraries.matrix.api.sync.SyncState
+import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
 import io.element.android.services.appnavstate.api.AppNavigationStateService
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.FlowPreview
@@ -96,6 +98,8 @@ class LoggedInFlowNode @AssistedInject constructor(
     private val shareEntryPoint: ShareEntryPoint,
     private val matrixClient: MatrixClient,
     private val sendingQueue: SendQueues,
+    private val logoutEntryPoint: LogoutEntryPoint,
+    private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
     snackbarDispatcher: SnackbarDispatcher,
 ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
     backstack = BackStack(
@@ -225,6 +229,9 @@ class LoggedInFlowNode @AssistedInject constructor(

         @Parcelize
         data class IncomingShare(val intent: Intent) : NavTarget
+
+        @Parcelize
+        data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
     }

     override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -271,6 +278,10 @@ class LoggedInFlowNode @AssistedInject constructor(
                     override fun onRoomDirectorySearchClick() {
                         backstack.push(NavTarget.RoomDirectorySearch)
                     }
+
+                    override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
+                        backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
+                    }
                 }
                 roomListEntryPoint
                     .nodeBuilder(this, buildContext)
@@ -407,6 +418,20 @@ class LoggedInFlowNode @AssistedInject constructor(
                     .params(ShareEntryPoint.Params(intent = navTarget.intent))
                     .build()
             }
+            is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> {
+                val callback = object : LogoutEntryPoint.Callback {
+                    override fun onChangeRecoveryKeyClick() {
+                        backstack.push(NavTarget.SecureBackup())
+                    }
+                }
+
+                logoutEntryPoint.nodeBuilder(this, buildContext)
+                    .onSuccessfulLogoutPendingAction {
+                        enableNativeSlidingSyncUseCase()
+                    }
+                    .callback(callback)
+                    .build()
+            }
         }
     }

diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt
index d90f039ec1b..ea952b01004 100644
--- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt
+++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt
@@ -16,6 +16,7 @@ interface LogoutEntryPoint : FeatureEntryPoint {
     fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder

     interface NodeBuilder {
+        fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder
         fun callback(callback: Callback): NodeBuilder
         fun build(): Node
     }
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt
index 7c7264a00f8..08de15c2ee1 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt
@@ -27,6 +27,15 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint {
                 return this
             }

+            override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder {
+                plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin {
+                    override fun onSuccessfulLogoutPendingAction() {
+                        action()
+                    }
+                }
+                return this
+            }
+
             override fun build(): Node {
                 return parentNode.createNode<LogoutNode>(buildContext, plugins)
             }
diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
index fb2c1585a05..dd9788cd1d5 100644
--- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
+++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
@@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor(
         plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
     }

+    interface SuccessfulLogoutPendingAction : Plugin {
+        fun onSuccessfulLogoutPendingAction()
+    }
+
+    private val customOnSuccessfulLogoutPendingAction = plugins<SuccessfulLogoutPendingAction>().firstOrNull()
+
     @Composable
     override fun View(modifier: Modifier) {
         val state = presenter.present()
@@ -41,7 +47,10 @@ class LogoutNode @AssistedInject constructor(
         LogoutView(
             state = state,
             onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
-            onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
+            onSuccessLogout = {
+                customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction()
+                onSuccessLogout(activity, isDark, it)
+            },
             onBackClick = ::navigateUp,
             modifier = modifier,
         )
diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
index 7070fbe9150..98ed717f7b9 100644
--- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
+++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt
@@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
         fun onRoomSettingsClick(roomId: RoomId)
         fun onReportBugClick()
         fun onRoomDirectorySearchClick()
+        fun onLogoutForNativeSlidingSyncMigrationNeeded()
     }
 }
diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts
index 5e3dc513c64..ce747859f5c 100644
--- a/features/roomlist/impl/build.gradle.kts
+++ b/features/roomlist/impl/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
     implementation(projects.libraries.push.api)
     implementation(projects.features.invite.api)
     implementation(projects.features.networkmonitor.api)
+    implementation(projects.features.logout.api)
     implementation(projects.features.leaveroom.api)
     implementation(projects.services.analytics.api)
     implementation(libs.androidx.datastore.preferences)
@@ -75,6 +76,7 @@ dependencies {
     testImplementation(projects.services.analytics.test)
     testImplementation(projects.services.toolbox.test)
     testImplementation(projects.features.networkmonitor.test)
+    testImplementation(projects.features.logout.test)
     testImplementation(projects.tests.testutils)
     testImplementation(projects.features.leaveroom.test)
 }
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt
index 4e586056fd1..6656ca8231c 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt
@@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
             aRoomsContentState(summaries = persistentListOf()),
             aSkeletonContentState(),
             anEmptyContentState(),
+            aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
         )
 }

diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
index 50a29f3ff76..9a8f2353ba4 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
@@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
 sealed interface RoomListEvents {
     data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
     data object DismissRequestVerificationPrompt : RoomListEvents
-    data object DismissRecoveryKeyPrompt : RoomListEvents
+    data object DismissBanner : RoomListEvents
     data object ToggleSearchResults : RoomListEvents
     data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
     data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
index bdb5425b826..1481027310b 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt
@@ -21,11 +21,14 @@ import dagger.assisted.AssistedInject
 import im.vector.app.features.analytics.plan.MobileScreen
 import io.element.android.anvilannotations.ContributesNode
 import io.element.android.features.invite.api.response.AcceptDeclineInviteView
+import io.element.android.features.logout.api.direct.DirectLogoutEvents
+import io.element.android.features.logout.api.direct.DirectLogoutView
 import io.element.android.features.roomlist.api.RoomListEntryPoint
 import io.element.android.features.roomlist.impl.components.RoomListMenuAction
 import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
 import io.element.android.libraries.di.SessionScope
 import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
 import io.element.android.services.analytics.api.AnalyticsService

 @ContributesNode(SessionScope::class)
@@ -36,6 +39,8 @@ class RoomListNode @AssistedInject constructor(
     private val inviteFriendsUseCase: InviteFriendsUseCase,
     private val analyticsService: AnalyticsService,
     private val acceptDeclineInviteView: AcceptDeclineInviteView,
+    private val directLogoutView: DirectLogoutView,
+    private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
 ) : Node(buildContext, plugins = plugins) {
     init {
         lifecycle.subscribe(
@@ -88,6 +93,7 @@ class RoomListNode @AssistedInject constructor(
     override fun View(modifier: Modifier) {
         val state = presenter.present()
         val activity = LocalContext.current as Activity
+
         RoomListView(
             state = state,
             onRoomClick = this::onRoomClick,
@@ -98,6 +104,13 @@ class RoomListNode @AssistedInject constructor(
             onRoomSettingsClick = this::onRoomSettingsClick,
             onMenuActionClick = { onMenuActionClick(activity, it) },
             onRoomDirectorySearchClick = this::onRoomDirectorySearchClick,
+            onMigrateToNativeSlidingSyncClick = {
+                if (state.directLogoutState.canDoDirectSignOut) {
+                    state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
+                } else {
+                    plugins<RoomListEntryPoint.Callback>().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() }
+                }
+            },
             modifier = modifier,
         ) {
             acceptDeclineInviteView.Render(
@@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor(
                 modifier = Modifier
             )
         }
+
+        directLogoutView.Render(state.directLogoutState) {
+            enableNativeSlidingSyncUseCase()
+        }
     }
 }
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index d2b45c0d061..2983b6b6952 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -29,6 +29,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
 import io.element.android.features.invite.api.response.InviteData
 import io.element.android.features.leaveroom.api.LeaveRoomEvent
 import io.element.android.features.leaveroom.api.LeaveRoomPresenter
+import io.element.android.features.logout.api.direct.DirectLogoutPresenter
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.api.NetworkStatus
 import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
     private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
     private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter,
     private val notificationCleaner: NotificationCleaner,
+    private val logoutPresenter: DirectLogoutPresenter,
 ) : Presenter<RoomListState> {
     private val encryptionService: EncryptionService = client.encryptionService()
     private val syncService: SyncService = client.syncService()
@@ -115,13 +117,15 @@ class RoomListPresenter @Inject constructor(

         val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }

+        val directLogoutState = logoutPresenter.present()
+
         fun handleEvents(event: RoomListEvents) {
             when (event) {
                 is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
                     updateVisibleRange(event.range)
                 }
                 RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
-                RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true
+                RoomListEvents.DismissBanner -> securityBannerDismissed = true
                 RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
                 is RoomListEvents.ShowContextMenu -> {
                     coroutineScope.showContextMenu(event, contextMenu)
@@ -161,6 +165,7 @@ class RoomListPresenter @Inject constructor(
             searchState = searchState,
             contentState = contentState,
             acceptDeclineInviteState = acceptDeclineInviteState,
+            directLogoutState = directLogoutState,
             eventSink = ::handleEvents,
         )
     }
@@ -168,6 +173,7 @@ class RoomListPresenter @Inject constructor(
     @Composable
     private fun securityBannerState(
         securityBannerDismissed: Boolean,
+        needsSlidingSyncMigration: Boolean,
     ): State<SecurityBannerState> {
         val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
         val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
@@ -185,6 +191,7 @@ class RoomListPresenter @Inject constructor(
                             RecoveryState.ENABLED -> SecurityBannerState.None
                         }
                     }
+                    needsSlidingSyncMigration -> SecurityBannerState.NeedsNativeSlidingSyncMigration
                     else -> SecurityBannerState.None
                 }
             }
@@ -209,11 +216,14 @@ class RoomListPresenter @Inject constructor(
                 loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
             }
         }
+        val needsSlidingSyncMigration by produceState(false) {
+            value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
+        }
         return when {
             showEmpty -> RoomListContentState.Empty
             showSkeleton -> RoomListContentState.Skeleton(count = 16)
             else -> {
-                val securityBannerState by securityBannerState(securityBannerDismissed)
+                val securityBannerState by securityBannerState(securityBannerDismissed, needsSlidingSyncMigration)
                 RoomListContentState.Rooms(
                     securityBannerState = securityBannerState,
                     fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
index bb95b85293f..ae9a721bfac 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
@@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl
 import androidx.compose.runtime.Immutable
 import io.element.android.features.invite.api.response.AcceptDeclineInviteState
 import io.element.android.features.leaveroom.api.LeaveRoomState
+import io.element.android.features.logout.api.direct.DirectLogoutState
 import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
 import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
 import io.element.android.features.roomlist.impl.search.RoomListSearchState
@@ -31,6 +32,7 @@ data class RoomListState(
     val searchState: RoomListSearchState,
     val contentState: RoomListContentState,
     val acceptDeclineInviteState: AcceptDeclineInviteState,
+    val directLogoutState: DirectLogoutState,
     val eventSink: (RoomListEvents) -> Unit,
 ) {
     val displayFilters = contentState is RoomListContentState.Rooms
@@ -59,6 +61,7 @@ enum class SecurityBannerState {
     None,
     SetUpRecovery,
     RecoveryKeyConfirmation,
+    NeedsNativeSlidingSyncMigration,
 }

 @Immutable
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
index 31499600ba1..b9d003e32c0 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt
@@ -12,6 +12,8 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
 import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
 import io.element.android.features.leaveroom.api.LeaveRoomState
 import io.element.android.features.leaveroom.api.aLeaveRoomState
+import io.element.android.features.logout.api.direct.DirectLogoutState
+import io.element.android.features.logout.api.direct.aDirectLogoutState
 import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
 import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
 import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
@@ -57,6 +59,7 @@ internal fun aRoomListState(
     filtersState: RoomListFiltersState = aRoomListFiltersState(),
     contentState: RoomListContentState = aRoomsContentState(),
     acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
+    directLogoutState: DirectLogoutState = aDirectLogoutState(),
     eventSink: (RoomListEvents) -> Unit = {}
 ) = RoomListState(
     matrixUser = matrixUser,
@@ -69,6 +72,7 @@ internal fun aRoomListState(
     searchState = searchState,
     contentState = contentState,
     acceptDeclineInviteState = acceptDeclineInviteState,
+    directLogoutState = directLogoutState,
     eventSink = eventSink,
 )

diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
index cfa5605c1e2..4db0f68e4e9 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
@@ -50,6 +50,7 @@ fun RoomListView(
     onRoomSettingsClick: (roomId: RoomId) -> Unit,
     onMenuActionClick: (RoomListMenuAction) -> Unit,
     onRoomDirectorySearchClick: () -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
     acceptDeclineInviteView: @Composable () -> Unit,
 ) {
@@ -76,6 +77,7 @@ fun RoomListView(
                 onOpenSettings = onSettingsClick,
                 onCreateRoomClick = onCreateRoomClick,
                 onMenuActionClick = onMenuActionClick,
+                onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
                 modifier = Modifier.padding(top = topPadding),
             )
             // This overlaid view will only be visible when state.displaySearchResults is true
@@ -105,6 +107,7 @@ private fun RoomListScaffold(
     onOpenSettings: () -> Unit,
     onCreateRoomClick: () -> Unit,
     onMenuActionClick: (RoomListMenuAction) -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     fun onRoomClick(room: RoomListRoomSummary) {
@@ -140,6 +143,7 @@ private fun RoomListScaffold(
                 onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
                 onRoomClick = ::onRoomClick,
                 onCreateRoomClick = onCreateRoomClick,
+                onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
                 modifier = Modifier
                     .padding(padding)
                     .consumeWindowInsets(padding)
@@ -180,5 +184,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
         onMenuActionClick = {},
         onRoomDirectorySearchClick = {},
         acceptDeclineInviteView = {},
+        onMigrateToNativeSlidingSyncClick = {},
     )
 }
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt
new file mode 100644
index 00000000000..6d058d6a8aa
--- /dev/null
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.roomlist.impl.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import io.element.android.features.roomlist.impl.R
+import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+
+@Composable
+internal fun NativeSlidingSyncMigrationBanner(
+    onContinueClick: () -> Unit,
+    onDismissClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    DialogLikeBannerMolecule(
+        modifier = modifier,
+        title = stringResource(R.string.banner_migrate_to_native_sliding_sync_title),
+        content = stringResource(R.string.banner_migrate_to_native_sliding_sync_description),
+        actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
+        onSubmitClick = onContinueClick,
+        onDismissClick = onDismissClick,
+    )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun NativeSlidingSyncMigrationBannerPreview() = ElementPreview {
+    NativeSlidingSyncMigrationBanner(
+        onContinueClick = {},
+        onDismissClick = {},
+    )
+}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt
index 8abbce3068c..d4af5a3fe54 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt
@@ -64,6 +64,7 @@ fun RoomListContentView(
     onConfirmRecoveryKeyClick: () -> Unit,
     onRoomClick: (RoomListRoomSummary) -> Unit,
     onCreateRoomClick: () -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     Box(modifier = modifier) {
@@ -85,6 +86,7 @@ fun RoomListContentView(
                     eventSink = eventSink,
                     onSetUpRecoveryClick = onSetUpRecoveryClick,
                     onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
+                    onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
                     onRoomClick = onRoomClick,
                 )
             }
@@ -133,6 +135,7 @@ private fun RoomsView(
     onSetUpRecoveryClick: () -> Unit,
     onConfirmRecoveryKeyClick: () -> Unit,
     onRoomClick: (RoomListRoomSummary) -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@@ -147,6 +150,7 @@ private fun RoomsView(
             onSetUpRecoveryClick = onSetUpRecoveryClick,
             onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
             onRoomClick = onRoomClick,
+            onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
             modifier = modifier.fillMaxSize(),
         )
     }
@@ -159,6 +163,7 @@ private fun RoomsViewList(
     onSetUpRecoveryClick: () -> Unit,
     onConfirmRecoveryKeyClick: () -> Unit,
     onRoomClick: (RoomListRoomSummary) -> Unit,
+    onMigrateToNativeSlidingSyncClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val lazyListState = rememberLazyListState()
@@ -185,7 +190,7 @@ private fun RoomsViewList(
                 item {
                     SetUpRecoveryKeyBanner(
                         onContinueClick = onSetUpRecoveryClick,
-                        onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
+                        onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
                     )
                 }
             }
@@ -193,7 +198,15 @@ private fun RoomsViewList(
                 item {
                     ConfirmRecoveryKeyBanner(
                         onContinueClick = onConfirmRecoveryKeyClick,
-                        onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
+                        onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
+                    )
+                }
+            }
+            SecurityBannerState.NeedsNativeSlidingSyncMigration -> {
+                item {
+                    NativeSlidingSyncMigrationBanner(
+                        onContinueClick = onMigrateToNativeSlidingSyncClick,
+                        onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
                     )
                 }
             }
@@ -278,5 +291,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
         onConfirmRecoveryKeyClick = {},
         onRoomClick = {},
         onCreateRoomClick = {},
+        onMigrateToNativeSlidingSyncClick = {},
     )
 }
diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml
index 6affb2bb168..ff6c626c63f 100644
--- a/features/roomlist/impl/src/main/res/values/localazy.xml
+++ b/features/roomlist/impl/src/main/res/values/localazy.xml
@@ -1,5 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="banner_migrate_to_native_sliding_sync_action">"Log Out & Upgrade"</string>
+  <string name="banner_migrate_to_native_sliding_sync_description">"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."</string>
+  <string name="banner_migrate_to_native_sliding_sync_title">"Upgrade available"</string>
   <string name="banner_set_up_recovery_content">"Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."</string>
   <string name="banner_set_up_recovery_title">"Set up recovery"</string>
   <string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt
index b9fd8d7fe04..966ce5b0b90 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt
@@ -39,6 +39,7 @@ fun DialogLikeBannerMolecule(
     onSubmitClick: () -> Unit,
     onDismissClick: (() -> Unit)?,
     modifier: Modifier = Modifier,
+    actionText: String = stringResource(CommonStrings.action_continue),
 ) {
     Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
         Surface(
@@ -74,7 +75,7 @@ fun DialogLikeBannerMolecule(
                 )
                 Spacer(modifier = Modifier.height(12.dp))
                 Button(
-                    text = stringResource(CommonStrings.action_continue),
+                    text = actionText,
                     size = ButtonSize.Medium,
                     modifier = Modifier.fillMaxWidth(),
                     onClick = onSubmitClick,
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
index 65dfa39c272..2591a5cfd31 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
@@ -126,4 +126,10 @@ interface MatrixClient : Closeable {
      */
     suspend fun getUrl(url: String): Result<String>
     suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomPreview>
+
+    /** Returns `true` if the home server supports native sliding sync. */
+    suspend fun isNativeSlidingSyncSupported(): Boolean
+
+    /** Returns `true` if the current session is using native sliding sync. */
+    fun isUsingNativeSlidingSync(): Boolean
 }
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 afdb9e99468..8077ef5e153 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
@@ -94,6 +94,7 @@ import org.matrix.rustcomponents.sdk.IgnoredUsersListener
 import org.matrix.rustcomponents.sdk.NotificationProcessSetup
 import org.matrix.rustcomponents.sdk.PowerLevels
 import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener
+import org.matrix.rustcomponents.sdk.SlidingSyncVersion
 import org.matrix.rustcomponents.sdk.TaskHandle
 import org.matrix.rustcomponents.sdk.use
 import timber.log.Timber
@@ -530,6 +531,14 @@ class RustMatrixClient(
         })
     }.buffer(Channel.UNLIMITED)

+    override suspend fun isNativeSlidingSyncSupported(): Boolean {
+        return client.availableSlidingSyncVersions().contains(SlidingSyncVersion.Native)
+    }
+
+    override fun isUsingNativeSlidingSync(): Boolean {
+        return client.session().slidingSyncVersion == SlidingSyncVersion.Native
+    }
+
     internal fun setDelegate(delegate: RustClientSessionDelegate) {
         client.setDelegate(delegate)
     }
diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts
index a8a1566987f..b9ab6630df1 100644
--- a/libraries/preferences/api/build.gradle.kts
+++ b/libraries/preferences/api/build.gradle.kts
@@ -17,4 +17,8 @@ dependencies {
     implementation(libs.coroutines.core)
     implementation(projects.libraries.matrix.api)
     implementation(libs.androidx.datastore.preferences)
+
+    testImplementation(projects.libraries.preferences.test)
+    testImplementation(libs.test.truth)
+    testImplementation(libs.coroutines.test)
 }
diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt
new file mode 100644
index 00000000000..e5bcbad18ea
--- /dev/null
+++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.preferences.api.store
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class EnableNativeSlidingSyncUseCase @Inject constructor(
+    private val appPreferencesStore: AppPreferencesStore,
+    private val appCoroutineScope: CoroutineScope,
+) {
+    operator fun invoke() {
+        appCoroutineScope.launch {
+            appPreferencesStore.setSimplifiedSlidingSyncEnabled(true)
+        }
+    }
+}
diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts
index 40a930d7e6f..ae7b6d58d4b 100644
--- a/samples/minimal/build.gradle.kts
+++ b/samples/minimal/build.gradle.kts
@@ -56,6 +56,7 @@ dependencies {
     implementation(projects.features.roomlist.impl)
     implementation(projects.features.leaveroom.impl)
     implementation(projects.features.login.impl)
+    implementation(projects.features.logout.impl)
     implementation(projects.features.networkmonitor.impl)
     implementation(projects.services.toolbox.impl)
     implementation(projects.libraries.featureflag.impl)
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
index a3516ba43f0..70f2a1f8a4a 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
 import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
 import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
 import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter
+import io.element.android.features.logout.impl.direct.DefaultDirectLogoutPresenter
 import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor
 import io.element.android.features.roomlist.impl.RoomListPresenter
 import io.element.android.features.roomlist.impl.RoomListView
@@ -144,6 +145,7 @@ class RoomListScreen(
             }
         },
         notificationCleaner = FakeNotificationCleaner(),
+        logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService),
     )

     @Composable
@@ -172,7 +174,8 @@ class RoomListScreen(
             modifier = modifier,
             acceptDeclineInviteView = {
                 AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {})
-            }
+            },
+            onMigrateToNativeSlidingSyncClick = {},
         )

         DisposableEffect(Unit) {
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index c6a256b1bd3..f7368257257 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -139,6 +139,7 @@
         "session_verification_banner_.*",
         "confirm_recovery_key_banner_.*",
         "banner\\.set_up_recovery\\..*",
+        "banner\\.migrate_to_native_sliding_sync\\..*",
         "full_screen_intent_banner_.*",
         "screen_migration_.*",
         "screen_invites_.*"

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 1b639d2b193..8b7294b165c 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
@@ -7,6 +7,7 @@

 package io.element.android.features.roomlist.impl

+import androidx.compose.runtime.Composable
 import app.cash.molecule.RecompositionMode
 import app.cash.molecule.moleculeFlow
 import app.cash.turbine.test
@@ -18,6 +19,8 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat
 import io.element.android.features.leaveroom.api.LeaveRoomEvent
 import io.element.android.features.leaveroom.api.LeaveRoomPresenter
 import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
+import io.element.android.features.logout.api.direct.DirectLogoutPresenter
+import io.element.android.features.logout.api.direct.aDirectLogoutState
 import io.element.android.features.networkmonitor.api.NetworkMonitor
 import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
 import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@@ -240,7 +243,7 @@ class RoomListPresenterTest {
             sessionVerificationService = FakeSessionVerificationService().apply {
                 givenNeedsSessionVerification(false)
             },
-            syncService = FakeSyncService(MutableStateFlow(SyncState.Running))
+            syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
         )
         val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
         val presenter = createRoomListPresenter(
@@ -268,7 +271,7 @@ class RoomListPresenterTest {
             assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
             encryptionService.emitRecoveryState(RecoveryState.DISABLED)
             assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
-            nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
+            nextState.eventSink(RoomListEvents.DismissBanner)
             val finalState = awaitItem()
             assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
             scope.cancel()
@@ -644,6 +647,10 @@ class RoomListPresenterTest {
         searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
         acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
         notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
+        logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter {
+            @Composable
+            override fun present() = aDirectLogoutState()
+        },
     ) = RoomListPresenter(
         client = client,
         networkMonitor = networkMonitor,
@@ -671,5 +678,6 @@ class RoomListPresenterTest {
         acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
         fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(),
         notificationCleaner = notificationCleaner,
+        logoutPresenter = logoutPresenter,
     )
 }
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
index 1c218c9aaa8..63327d0d49a 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
@@ -68,7 +68,7 @@ class RoomListViewTest {

         val close = rule.activity.getString(CommonStrings.action_close)
         rule.onNodeWithContentDescription(close).performClick()
-        eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
+        eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
     }

     @Test
@@ -86,7 +86,7 @@ class RoomListViewTest {

         val close = rule.activity.getString(CommonStrings.action_close)
         rule.onNodeWithContentDescription(close).performClick()
-        eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt)
+        eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
     }

     @Test
@@ -232,6 +232,21 @@ class RoomListViewTest {
             listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)),
         )
     }
+
+    @Test
+    fun `clicking on logout and migrate calls the migration clicked callback`() {
+        val state = aRoomListState(
+            contentState = aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
+            eventSink = {},
+        )
+        ensureCalledOnce { callback ->
+            rule.setRoomListView(
+                state = state,
+                onMigrateToNativeSlidingSyncClick = callback,
+            )
+            rule.clickOn(R.string.banner_migrate_to_native_sliding_sync_action)
+        }
+    }
 }

 private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView(
@@ -244,6 +259,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
     onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
     onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
     onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
+    onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled()
 ) {
     setContent {
         RoomListView(
@@ -256,6 +272,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
             onRoomSettingsClick = onRoomSettingsClick,
             onMenuActionClick = onMenuActionClick,
             onRoomDirectorySearchClick = onRoomDirectorySearchClick,
+            onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
             acceptDeclineInviteView = { },
         )
     }
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
index 040f24fb959..f4085bfb515 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
@@ -77,6 +77,8 @@ class FakeMatrixClient(
     private val clearCacheLambda: () -> Unit = { lambdaError() },
     private val userIdServerNameLambda: () -> String = { lambdaError() },
     private val getUrlLambda: (String) -> Result<String> = { lambdaError() },
+    var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true },
+    var isUsingNativeSlidingSyncLambda: () -> Boolean = { true },
 ) : MatrixClient {
     var setDisplayNameCalled: Boolean = false
         private set
@@ -316,4 +318,12 @@ class FakeMatrixClient(
     override suspend fun getUrl(url: String): Result<String> {
         return getUrlLambda(url)
     }
+
+    override suspend fun isNativeSlidingSyncSupported(): Boolean {
+        return isNativeSlidingSyncSupportedLambda()
+    }
+
+    override fun isUsingNativeSlidingSync(): Boolean {
+        return isUsingNativeSlidingSyncLambda()
+    }
 }
diff --git a/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt
new file mode 100644
index 00000000000..315c8f91d60
--- /dev/null
+++ b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.preferences.api.store
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class EnableNativeSlidingSyncUseCaseTest {
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun `ensure that the use case sets the simplified sliding sync enabled flag`() = runTest {
+        val preferencesStore = InMemoryAppPreferencesStore()
+        val useCase = EnableNativeSlidingSyncUseCase(preferencesStore, this)
+        assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
+
+        useCase()
+        advanceUntilIdle()
+
+        assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
+    }
+}
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png
new file mode 100644
index 00000000000..d5c79327d58
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:45f8b8767d1a54afde878f1c853a7fd6dd5823d04db3b75a7c18c5d6fc742d8e
+size 36110
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png
new file mode 100644
index 00000000000..f875fdea799
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6849a6c73341edc3f37fd6ef968c07e63277d18f833028386012fa2a438ade7e
+size 34946
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png
new file mode 100644
index 00000000000..dfcee5c2f68
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f4ee87f2ae823b3d026b70d6b91463ba6aa3ad7e454b915c0eba3901b219169b
+size 72188
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png
new file mode 100644
index 00000000000..ff6ac651553
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:863e7f66501f357728aa048560dc59fccd63bff8fc4b648296d82db119b0137d
+size 71031


Delete old log files

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

## Content

<!-- Describe shortly what has been changed -->
Add an application migration to delete the logs which use previous filename format.

Also add missing test and fix a bug where migrating application was performed on fresh install. This had no effect but was not clean.

## Motivation and context

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

Closes #3406

## 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 -->

- Step 1
- Step 2
- Step ...

## Tested devices

- [ ] Physical
- [ ] 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 title will be used in the release note, it clearly define what will change for the user
- [ ] 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

old logfiles are not being cleaned up

old logfiles are not being cleaned up
https://github.com/element-hq/element-x-android-rageshakes/issues/2619 contains a total of 1103 logfiles, many of which are quite old.

I gather that the intention is to delete logfiles after a week, but that has not happened in this case.

The filenames of the old logs fall into two groups:
 * old-style `logs-NNNN.log.gz`
 * new-style: `logs.YYYY-MM-DD-HH.log.gz`. Obviously there should be a few of these, but the older ones are over a month old

diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts
index 69789750709..69a1c2bfe92 100644
--- a/features/migration/impl/build.gradle.kts
+++ b/features/migration/impl/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
     testImplementation(libs.test.robolectric)
     testImplementation(libs.test.truth)
     testImplementation(libs.test.turbine)
+    testImplementation(projects.libraries.matrix.test)
     testImplementation(projects.libraries.sessionStorage.implMemory)
     testImplementation(projects.libraries.sessionStorage.test)
     testImplementation(projects.libraries.preferences.test)
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
index a0158061e29..3a0c211421d 100644
--- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt
@@ -46,7 +46,7 @@ class DefaultMigrationStore @Inject constructor(

     override fun applicationMigrationVersion(): Flow<Int> {
         return store.data.map { prefs ->
-            prefs[applicationMigrationVersion] ?: 0
+            prefs[applicationMigrationVersion] ?: -1
         }
     }
 }
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt
index d157566e24a..4c452b688aa 100644
--- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationPresenter.kt
@@ -53,6 +53,12 @@ class MigrationPresenter @Inject constructor(

         LaunchedEffect(migrationStoreVersion) {
             val migrationValue = migrationStoreVersion ?: return@LaunchedEffect
+            if (migrationValue == -1) {
+                // Fresh install, no migration needed
+                Timber.d("Fresh install, no migration needed.")
+                migrationStore.setApplicationMigrationVersion(lastMigration)
+                return@LaunchedEffect
+            }
             if (migrationValue == lastMigration) {
                 Timber.d("Current app migration version: $migrationValue. No migration needed.")
                 migrationAction = AsyncData.Success(Unit)
diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt
new file mode 100644
index 00000000000..c0da439f9b9
--- /dev/null
+++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 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.features.migration.impl.migrations
+
+import com.squareup.anvil.annotations.ContributesMultibinding
+import io.element.android.features.rageshake.api.logs.LogFilesRemover
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+/**
+ * Delete the previous log files.
+ */
+@ContributesMultibinding(AppScope::class)
+class AppMigration07 @Inject constructor(
+    private val logFilesRemover: LogFilesRemover,
+) : AppMigration {
+    override val order: Int = 7
+
+    override suspend fun migrate() {
+        logFilesRemover.perform { file ->
+            file.name.startsWith("logs-")
+        }
+    }
+}
diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt
index dd731330600..2b1fa29261d 100644
--- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt
+++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt
@@ -16,6 +16,12 @@

 package io.element.android.features.rageshake.api.logs

+import java.io.File
+
 interface LogFilesRemover {
-    suspend fun perform()
+    /**
+     * Perform the log files removal.
+     * @param predicate a predicate to filter the files to remove. By default, all files are removed.
+     */
+    suspend fun perform(predicate: (File) -> Boolean = { true })
 }
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt
index c0c7c8c3462..dd551d517e6 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/DefaultLogFilesRemover.kt
@@ -20,13 +20,14 @@ import com.squareup.anvil.annotations.ContributesBinding
 import io.element.android.features.rageshake.api.logs.LogFilesRemover
 import io.element.android.features.rageshake.impl.reporter.DefaultBugReporter
 import io.element.android.libraries.di.AppScope
+import java.io.File
 import javax.inject.Inject

 @ContributesBinding(AppScope::class)
 class DefaultLogFilesRemover @Inject constructor(
     private val bugReporter: DefaultBugReporter,
 ) : LogFilesRemover {
-    override suspend fun perform() {
-        bugReporter.deleteAllFiles()
+    override suspend fun perform(predicate: (File) -> Boolean) {
+        bugReporter.deleteAllFiles(predicate)
     }
 }
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
index 72e668c3f48..925577ce77b 100755
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
@@ -301,9 +301,11 @@ class DefaultBugReporter @Inject constructor(
         }
     }

-    suspend fun deleteAllFiles() {
+    suspend fun deleteAllFiles(predicate: (File) -> Boolean) {
         withContext(coroutineDispatchers.io) {
-            getLogFiles().forEach { it.safeDelete() }
+            getLogFiles()
+                .filter(predicate)
+                .forEach { it.safeDelete() }
         }
     }

diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
index 29be8682e34..26cb0df3c1a 100644
--- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/MigrationPresenterTest.kt
@@ -35,6 +35,32 @@ class MigrationPresenterTest {
     @get:Rule
     val warmUpRule = WarmUpRule()

+    @Test
+    fun `present - no migration should occurs on fresh installation, and last version should be stored`() = runTest {
+        val migrations = (1..10).map { order ->
+            FakeAppMigration(
+                order = order,
+                migrateLambda = LambdaNoParamRecorder(ensureNeverCalled = true) { },
+            )
+        }
+        val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1)
+        val presenter = createPresenter(
+            migrationStore = store,
+            migrations = migrations.toSet(),
+        )
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
+            skipItems(1)
+            awaitItem().also { state ->
+                assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
+            }
+            assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
+        }
+    }
+
     @Test
     fun `present - no migration should occurs if ApplicationMigrationVersion is the last one`() = runTest {
         val migrations = (1..10).map { FakeAppMigration(it) }
@@ -89,7 +115,7 @@ private fun createPresenter(

 private class FakeAppMigration(
     override val order: Int,
-    var migrateLambda: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
+    val migrateLambda: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
 ) : AppMigration {
     override suspend fun migrate() {
         migrateLambda()
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt
new file mode 100644
index 00000000000..dd2e969364a
--- /dev/null
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05Test.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2024 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.features.migration.impl.migrations
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
+import io.element.android.libraries.sessionstorage.test.aSessionData
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import java.io.File
+
+class AppMigration05Test {
+    @Test
+    fun `empty session path should be set to an expected path`() = runTest {
+        val sessionStore = InMemorySessionStore().apply {
+            updateData(
+                aSessionData(
+                    sessionId = A_SESSION_ID,
+                    sessionPath = "",
+                )
+            )
+        }
+        val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
+        migration.migrate()
+        val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
+        assertThat(storedData.sessionPath).isEqualTo("/a/path/${A_SESSION_ID.value.replace(':', '_')}")
+    }
+
+    @Test
+    fun `non empty session path should not be impacted by the migration`() = runTest {
+        val sessionStore = InMemorySessionStore().apply {
+            updateData(
+                aSessionData(
+                    sessionId = A_SESSION_ID,
+                    sessionPath = "/a/path/existing",
+                )
+            )
+        }
+        val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path"))
+        migration.migrate()
+        val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
+        assertThat(storedData.sessionPath).isEqualTo("/a/path/existing")
+    }
+}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt
new file mode 100644
index 00000000000..be5a8879dea
--- /dev/null
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration06Test.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2024 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.features.migration.impl.migrations
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
+import io.element.android.libraries.sessionstorage.test.aSessionData
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import java.io.File
+
+class AppMigration06Test {
+    @Test
+    fun `empty cache path should be set to an expected path`() = runTest {
+        val sessionStore = InMemorySessionStore().apply {
+            updateData(
+                aSessionData(
+                    sessionId = A_SESSION_ID,
+                    sessionPath = "/a/path/to/a/session/AN_ID",
+                    cachePath = "",
+                )
+            )
+        }
+        val migration = AppMigration06(sessionStore = sessionStore, cacheDirectory = File("/a/path/cache"))
+        migration.migrate()
+        val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
+        assertThat(storedData.cachePath).isEqualTo("/a/path/cache/AN_ID")
+    }
+
+    @Test
+    fun `non empty cache path should not be impacted by the migration`() = runTest {
+        val sessionStore = InMemorySessionStore().apply {
+            updateData(
+                aSessionData(
+                    sessionId = A_SESSION_ID,
+                    cachePath = "/a/path/existing",
+                )
+            )
+        }
+        val migration = AppMigration05(sessionStore = sessionStore, baseDirectory = File("/a/path/cache"))
+        migration.migrate()
+        val storedData = sessionStore.getSession(A_SESSION_ID.value)!!
+        assertThat(storedData.cachePath).isEqualTo("/a/path/existing")
+    }
+}
diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt
new file mode 100644
index 00000000000..95816b898f8
--- /dev/null
+++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration07Test.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2024 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.features.migration.impl.migrations
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import java.io.File
+
+class AppMigration07Test {
+    @Test
+    fun `test migration`() = runTest {
+        val performLambda = lambdaRecorder<(File) -> Boolean, Unit> { predicate ->
+            // Test the predicate
+            assertThat(predicate(File("logs-0433.log.gz"))).isTrue()
+            assertThat(predicate(File("logs.2024-08-01-20.log.gz"))).isFalse()
+        }
+        val logsFileRemover = FakeLogFilesRemover(performLambda = performLambda)
+        val migration = AppMigration07(logsFileRemover)
+        migration.migrate()
+        performLambda.assertions().isCalledOnce()
+    }
+}
diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
index 9b07b1c9428..0863962f6d6 100644
--- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
+++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt
@@ -32,7 +32,7 @@ import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolde
 import io.element.android.libraries.architecture.AsyncAction
 import io.element.android.libraries.matrix.test.A_FAILURE_REASON
 import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Rule
@@ -120,11 +120,11 @@ class BugReportPresenterTest {

     @Test
     fun `present - reset all`() = runTest {
-        val logFilesRemoverLambda = lambdaRecorder { -> }
+        val logFilesRemover = FakeLogFilesRemover()
         val presenter = createPresenter(
             crashDataStore = FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
             screenshotHolder = FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
-            logFilesRemover = FakeLogFilesRemover(logFilesRemoverLambda),
+            logFilesRemover = logFilesRemover,
         )
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
@@ -136,7 +136,7 @@ class BugReportPresenterTest {
             initialState.eventSink.invoke(BugReportEvents.ResetAll)
             val resetState = awaitItem()
             assertThat(resetState.hasCrashLogs).isFalse()
-            logFilesRemoverLambda.assertions().isCalledOnce()
+            logFilesRemover.performLambda.assertions().isCalledOnce()
             // TODO Make it live assertThat(resetState.screenshotUri).isNull()
         }
     }
@@ -245,7 +245,7 @@ class BugReportPresenterTest {
         bugReporter: BugReporter = FakeBugReporter(),
         crashDataStore: CrashDataStore = FakeCrashDataStore(),
         screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(),
-        logFilesRemover: LogFilesRemover = FakeLogFilesRemover(lambdaRecorder(ensureNeverCalled = true) { -> }),
+        logFilesRemover: LogFilesRemover = FakeLogFilesRemover(LambdaOneParamRecorder(ensureNeverCalled = true) { }),
     ) = BugReportPresenter(
         bugReporter = bugReporter,
         crashDataStore = crashDataStore,
diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt
index a416f2eeb4a..0999a762930 100644
--- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt
+++ b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeLogFilesRemover.kt
@@ -17,13 +17,14 @@
 package io.element.android.features.rageshake.test.logs

 import io.element.android.features.rageshake.api.logs.LogFilesRemover
-import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
+import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
 import io.element.android.tests.testutils.lambda.lambdaRecorder
+import java.io.File

 class FakeLogFilesRemover(
-    var performLambda: LambdaNoParamRecorder<Unit> = lambdaRecorder { -> },
+    val performLambda: LambdaOneParamRecorder<(File) -> Boolean, Unit> = lambdaRecorder<(File) -> Boolean, Unit> { },
 ) : LogFilesRemover {
-    override suspend fun perform() {
-        performLambda()
+    override suspend fun perform(predicate: (File) -> Boolean) {
+        performLambda(predicate)
     }
 }
diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt
index c33ab78d619..659d59e8ecc 100644
--- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt
+++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt
@@ -23,6 +23,8 @@ import io.element.android.libraries.sessionstorage.api.SessionData
 fun aSessionData(
     sessionId: SessionId = SessionId("@alice:server.org"),
     isTokenValid: Boolean = false,
+    sessionPath: String = "/a/path/to/a/session",
+    cachePath: String = "/a/path/to/a/cache",
 ): SessionData {
     return SessionData(
         userId = sessionId.value,
@@ -36,7 +38,7 @@ fun aSessionData(
         isTokenValid = isTokenValid,
         loginType = LoginType.UNKNOWN,
         passphrase = null,
-        sessionPath = "/a/path/to/a/session",
-        cachePath = "/a/path/to/a/cache",
+        sessionPath = sessionPath,
+        cachePath = cachePath,
     )
 }