Skip to content

Commit 67e262f

Browse files
jmartinespElementBot
andauthored
Add banner for optional migration to simplified sliding sync (#3429)
* Add banner for optional migration to native sliding sync - 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. * Update screenshots * Make sure the sliding sync migration banner has lower priority than the encryption setup ones --------- Co-authored-by: ElementBot <[email protected]>
1 parent 7549d5f commit 67e262f

File tree

32 files changed

+283
-12
lines changed

32 files changed

+283
-12
lines changed

appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
4040
import io.element.android.features.ftue.api.FtueEntryPoint
4141
import io.element.android.features.ftue.api.state.FtueService
4242
import io.element.android.features.ftue.api.state.FtueState
43+
import io.element.android.features.logout.api.LogoutEntryPoint
4344
import io.element.android.features.networkmonitor.api.NetworkMonitor
4445
import io.element.android.features.networkmonitor.api.NetworkStatus
4546
import io.element.android.features.preferences.api.PreferencesEntryPoint
@@ -65,6 +66,7 @@ import io.element.android.libraries.matrix.api.core.UserId
6566
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
6667
import io.element.android.libraries.matrix.api.permalink.PermalinkData
6768
import io.element.android.libraries.matrix.api.sync.SyncState
69+
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
6870
import io.element.android.services.appnavstate.api.AppNavigationStateService
6971
import kotlinx.coroutines.CoroutineScope
7072
import kotlinx.coroutines.FlowPreview
@@ -96,6 +98,8 @@ class LoggedInFlowNode @AssistedInject constructor(
9698
private val shareEntryPoint: ShareEntryPoint,
9799
private val matrixClient: MatrixClient,
98100
private val sendingQueue: SendQueues,
101+
private val logoutEntryPoint: LogoutEntryPoint,
102+
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
99103
snackbarDispatcher: SnackbarDispatcher,
100104
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
101105
backstack = BackStack(
@@ -225,6 +229,9 @@ class LoggedInFlowNode @AssistedInject constructor(
225229

226230
@Parcelize
227231
data class IncomingShare(val intent: Intent) : NavTarget
232+
233+
@Parcelize
234+
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
228235
}
229236

230237
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -271,6 +278,10 @@ class LoggedInFlowNode @AssistedInject constructor(
271278
override fun onRoomDirectorySearchClick() {
272279
backstack.push(NavTarget.RoomDirectorySearch)
273280
}
281+
282+
override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
283+
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
284+
}
274285
}
275286
roomListEntryPoint
276287
.nodeBuilder(this, buildContext)
@@ -407,6 +418,20 @@ class LoggedInFlowNode @AssistedInject constructor(
407418
.params(ShareEntryPoint.Params(intent = navTarget.intent))
408419
.build()
409420
}
421+
is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> {
422+
val callback = object : LogoutEntryPoint.Callback {
423+
override fun onChangeRecoveryKeyClick() {
424+
backstack.push(NavTarget.SecureBackup())
425+
}
426+
}
427+
428+
logoutEntryPoint.nodeBuilder(this, buildContext)
429+
.onSuccessfulLogoutPendingAction {
430+
enableNativeSlidingSyncUseCase()
431+
}
432+
.callback(callback)
433+
.build()
434+
}
410435
}
411436
}
412437

features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface LogoutEntryPoint : FeatureEntryPoint {
1616
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
1717

1818
interface NodeBuilder {
19+
fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder
1920
fun callback(callback: Callback): NodeBuilder
2021
fun build(): Node
2122
}

features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint {
2727
return this
2828
}
2929

30+
override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder {
31+
plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin {
32+
override fun onSuccessfulLogoutPendingAction() {
33+
action()
34+
}
35+
}
36+
return this
37+
}
38+
3039
override fun build(): Node {
3140
return parentNode.createNode<LogoutNode>(buildContext, plugins)
3241
}

features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor(
3333
plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
3434
}
3535

36+
interface SuccessfulLogoutPendingAction : Plugin {
37+
fun onSuccessfulLogoutPendingAction()
38+
}
39+
40+
private val customOnSuccessfulLogoutPendingAction = plugins<SuccessfulLogoutPendingAction>().firstOrNull()
41+
3642
@Composable
3743
override fun View(modifier: Modifier) {
3844
val state = presenter.present()
@@ -41,7 +47,10 @@ class LogoutNode @AssistedInject constructor(
4147
LogoutView(
4248
state = state,
4349
onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
44-
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
50+
onSuccessLogout = {
51+
customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction()
52+
onSuccessLogout(activity, isDark, it)
53+
},
4554
onBackClick = ::navigateUp,
4655
modifier = modifier,
4756
)

features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
2929
fun onRoomSettingsClick(roomId: RoomId)
3030
fun onReportBugClick()
3131
fun onRoomDirectorySearchClick()
32+
fun onLogoutForNativeSlidingSyncMigrationNeeded()
3233
}
3334
}

features/roomlist/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ dependencies {
4949
implementation(projects.libraries.push.api)
5050
implementation(projects.features.invite.api)
5151
implementation(projects.features.networkmonitor.api)
52+
implementation(projects.features.logout.api)
5253
implementation(projects.features.leaveroom.api)
5354
implementation(projects.services.analytics.api)
5455
implementation(libs.androidx.datastore.preferences)
@@ -75,6 +76,7 @@ dependencies {
7576
testImplementation(projects.services.analytics.test)
7677
testImplementation(projects.services.toolbox.test)
7778
testImplementation(projects.features.networkmonitor.test)
79+
testImplementation(projects.features.logout.test)
7880
testImplementation(projects.tests.testutils)
7981
testImplementation(projects.features.leaveroom.test)
8082
}

features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
2020
aRoomsContentState(summaries = persistentListOf()),
2121
aSkeletonContentState(),
2222
anEmptyContentState(),
23+
aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
2324
)
2425
}
2526

features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
1313
sealed interface RoomListEvents {
1414
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
1515
data object DismissRequestVerificationPrompt : RoomListEvents
16-
data object DismissRecoveryKeyPrompt : RoomListEvents
16+
data object DismissBanner : RoomListEvents
1717
data object ToggleSearchResults : RoomListEvents
1818
data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
1919
data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents

features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ import dagger.assisted.AssistedInject
2121
import im.vector.app.features.analytics.plan.MobileScreen
2222
import io.element.android.anvilannotations.ContributesNode
2323
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
24+
import io.element.android.features.logout.api.direct.DirectLogoutEvents
25+
import io.element.android.features.logout.api.direct.DirectLogoutView
2426
import io.element.android.features.roomlist.api.RoomListEntryPoint
2527
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
2628
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
2729
import io.element.android.libraries.di.SessionScope
2830
import io.element.android.libraries.matrix.api.core.RoomId
31+
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
2932
import io.element.android.services.analytics.api.AnalyticsService
3033

3134
@ContributesNode(SessionScope::class)
@@ -36,6 +39,8 @@ class RoomListNode @AssistedInject constructor(
3639
private val inviteFriendsUseCase: InviteFriendsUseCase,
3740
private val analyticsService: AnalyticsService,
3841
private val acceptDeclineInviteView: AcceptDeclineInviteView,
42+
private val directLogoutView: DirectLogoutView,
43+
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
3944
) : Node(buildContext, plugins = plugins) {
4045
init {
4146
lifecycle.subscribe(
@@ -88,6 +93,7 @@ class RoomListNode @AssistedInject constructor(
8893
override fun View(modifier: Modifier) {
8994
val state = presenter.present()
9095
val activity = LocalContext.current as Activity
96+
9197
RoomListView(
9298
state = state,
9399
onRoomClick = this::onRoomClick,
@@ -98,6 +104,13 @@ class RoomListNode @AssistedInject constructor(
98104
onRoomSettingsClick = this::onRoomSettingsClick,
99105
onMenuActionClick = { onMenuActionClick(activity, it) },
100106
onRoomDirectorySearchClick = this::onRoomDirectorySearchClick,
107+
onMigrateToNativeSlidingSyncClick = {
108+
if (state.directLogoutState.canDoDirectSignOut) {
109+
state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
110+
} else {
111+
plugins<RoomListEntryPoint.Callback>().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() }
112+
}
113+
},
101114
modifier = modifier,
102115
) {
103116
acceptDeclineInviteView.Render(
@@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor(
107120
modifier = Modifier
108121
)
109122
}
123+
124+
directLogoutView.Render(state.directLogoutState) {
125+
enableNativeSlidingSyncUseCase()
126+
}
110127
}
111128
}

features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
2929
import io.element.android.features.invite.api.response.InviteData
3030
import io.element.android.features.leaveroom.api.LeaveRoomEvent
3131
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
32+
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
3233
import io.element.android.features.networkmonitor.api.NetworkMonitor
3334
import io.element.android.features.networkmonitor.api.NetworkStatus
3435
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
8889
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
8990
private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter,
9091
private val notificationCleaner: NotificationCleaner,
92+
private val logoutPresenter: DirectLogoutPresenter,
9193
) : Presenter<RoomListState> {
9294
private val encryptionService: EncryptionService = client.encryptionService()
9395
private val syncService: SyncService = client.syncService()
@@ -115,13 +117,15 @@ class RoomListPresenter @Inject constructor(
115117

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

120+
val directLogoutState = logoutPresenter.present()
121+
118122
fun handleEvents(event: RoomListEvents) {
119123
when (event) {
120124
is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
121125
updateVisibleRange(event.range)
122126
}
123127
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
124-
RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true
128+
RoomListEvents.DismissBanner -> securityBannerDismissed = true
125129
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
126130
is RoomListEvents.ShowContextMenu -> {
127131
coroutineScope.showContextMenu(event, contextMenu)
@@ -161,13 +165,15 @@ class RoomListPresenter @Inject constructor(
161165
searchState = searchState,
162166
contentState = contentState,
163167
acceptDeclineInviteState = acceptDeclineInviteState,
168+
directLogoutState = directLogoutState,
164169
eventSink = ::handleEvents,
165170
)
166171
}
167172

168173
@Composable
169174
private fun securityBannerState(
170175
securityBannerDismissed: Boolean,
176+
needsSlidingSyncMigration: Boolean,
171177
): State<SecurityBannerState> {
172178
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
173179
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
@@ -185,6 +191,7 @@ class RoomListPresenter @Inject constructor(
185191
RecoveryState.ENABLED -> SecurityBannerState.None
186192
}
187193
}
194+
needsSlidingSyncMigration -> SecurityBannerState.NeedsNativeSlidingSyncMigration
188195
else -> SecurityBannerState.None
189196
}
190197
}
@@ -209,11 +216,14 @@ class RoomListPresenter @Inject constructor(
209216
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
210217
}
211218
}
219+
val needsSlidingSyncMigration by produceState(false) {
220+
value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
221+
}
212222
return when {
213223
showEmpty -> RoomListContentState.Empty
214224
showSkeleton -> RoomListContentState.Skeleton(count = 16)
215225
else -> {
216-
val securityBannerState by securityBannerState(securityBannerDismissed)
226+
val securityBannerState by securityBannerState(securityBannerDismissed, needsSlidingSyncMigration)
217227
RoomListContentState.Rooms(
218228
securityBannerState = securityBannerState,
219229
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),

0 commit comments

Comments
 (0)