Merge branch 'develop' into feature/fga/pinned_messages_list_remove_reaction

This commit is contained in:
ganfra 2024-09-10 11:25:40 +02:00 committed by GitHub
commit e059cdd916
82 changed files with 375 additions and 907 deletions

View file

@ -1,3 +1,35 @@
Changes in Element X v0.5.3 (2024-09-10)
========================================
### ✨ Features
* Add banner for optional migration to simplified sliding sync by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3429
### 🙌 Improvements
* Timeline : remove the encrypted history banner by @ganfra in https://github.com/element-hq/element-x-android/pull/3410
### 🐛 Bugfixes
* Fix new logins with Simplified SS using the proxy by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3417
* Ensure Call is not hang up when user is asked to grant system permissions by @bmarty in https://github.com/element-hq/element-x-android/pull/3419
* Wait for a room with joined state in `/sync` after creating it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3421
* [Bugfix] : fix self verification flow by @ganfra in https://github.com/element-hq/element-x-android/pull/3426
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3425
### 🚧 In development 🚧
* [Feature] Pinned messages list by @ganfra in https://github.com/element-hq/element-x-android/pull/3392
* Pinned messages banner : adjust indicator to match design. by @ganfra in https://github.com/element-hq/element-x-android/pull/3415
### Dependency upgrades
* Update plugin dependencycheck to v10.0.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3372
* Update plugin detekt to v1.23.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3424
### Others
* Delete old log files by @bmarty in https://github.com/element-hq/element-x-android/pull/3413
* Recovery key formatting and wording iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/3409
* Change license to AGPL by @bmarty in https://github.com/element-hq/element-x-android/pull/3422
* Remove Wait list screen by @bmarty in https://github.com/element-hq/element-x-android/pull/3428
Changes in Element X v0.5.2 (2024-09-05) Changes in Element X v0.5.2 (2024-09-05)
========================================= =========================================

View file

@ -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.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState 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.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint 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.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState 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 io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@ -96,6 +98,8 @@ class LoggedInFlowNode @AssistedInject constructor(
private val shareEntryPoint: ShareEntryPoint, private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues, private val sendingQueue: SendQueues,
private val logoutEntryPoint: LogoutEntryPoint,
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
snackbarDispatcher: SnackbarDispatcher, snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>( ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -225,6 +229,9 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class IncomingShare(val intent: Intent) : NavTarget data class IncomingShare(val intent: Intent) : NavTarget
@Parcelize
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -271,6 +278,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onRoomDirectorySearchClick() { override fun onRoomDirectorySearchClick() {
backstack.push(NavTarget.RoomDirectorySearch) backstack.push(NavTarget.RoomDirectorySearch)
} }
override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
}
} }
roomListEntryPoint roomListEntryPoint
.nodeBuilder(this, buildContext) .nodeBuilder(this, buildContext)
@ -407,6 +418,20 @@ class LoggedInFlowNode @AssistedInject constructor(
.params(ShareEntryPoint.Params(intent = navTarget.intent)) .params(ShareEntryPoint.Params(intent = navTarget.intent))
.build() .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()
}
} }
} }

View file

@ -0,0 +1,2 @@
Main changes in this version: mainly bug fixes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -19,7 +19,6 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.singleTop import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -31,10 +30,8 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode
import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode
import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode
import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode
import io.element.android.features.login.impl.screens.waitlistscreen.WaitListNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
@ -112,9 +109,6 @@ class LoginFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object LoginPassword : NavTarget data object LoginPassword : NavTarget
@Parcelize
data class WaitList(val loginFormState: LoginFormState) : NavTarget
@Parcelize @Parcelize
data class OidcView(val oidcDetails: OidcDetails) : NavTarget data class OidcView(val oidcDetails: OidcDetails) : NavTarget
} }
@ -181,27 +175,11 @@ class LoginFlowNode @AssistedInject constructor(
createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback)) createNode<SearchAccountProviderNode>(buildContext, plugins = listOf(callback))
} }
NavTarget.LoginPassword -> { NavTarget.LoginPassword -> {
val callback = object : LoginPasswordNode.Callback { createNode<LoginPasswordNode>(buildContext)
override fun onWaitListError(loginFormState: LoginFormState) {
backstack.newRoot(NavTarget.WaitList(loginFormState))
}
}
createNode<LoginPasswordNode>(buildContext, plugins = listOf(callback))
} }
is NavTarget.OidcView -> { is NavTarget.OidcView -> {
oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url) oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url)
} }
is NavTarget.WaitList -> {
val inputs = WaitListNode.Inputs(
loginFormState = navTarget.loginFormState,
)
val callback = object : WaitListNode.Callback {
override fun onCancelClick() {
navigateUp()
}
}
createNode<WaitListNode>(buildContext, plugins = listOf(callback, inputs))
}
} }
} }

View file

@ -1,14 +0,0 @@
/*
* Copyright 2023, 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.login.impl.error
import io.element.android.libraries.core.bool.orFalse
fun Throwable.isWaitListError(): Boolean {
return message?.contains("IO_ELEMENT_X_WAIT_LIST").orFalse()
}

View file

@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
@ -24,14 +23,6 @@ class LoginPasswordNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: LoginPasswordPresenter, private val presenter: LoginPasswordPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onWaitListError(loginFormState: LoginFormState)
}
private fun onWaitListError(loginFormState: LoginFormState) {
plugins<Callback>().forEach { it.onWaitListError(loginFormState) }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
@ -39,7 +30,6 @@ class LoginPasswordNode @AssistedInject constructor(
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onWaitListError = ::onWaitListError,
) )
} }
} }

View file

@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.isWaitListError
import io.element.android.features.login.impl.error.loginError import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
@ -72,7 +71,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun LoginPasswordView( fun LoginPasswordView(
state: LoginPasswordState, state: LoginPasswordState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onWaitListError: (LoginFormState) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isLoading by remember(state.loginAction) { val isLoading by remember(state.loginAction) {
@ -149,16 +147,9 @@ fun LoginPasswordView(
} }
if (state.loginAction is AsyncData.Failure) { if (state.loginAction is AsyncData.Failure) {
when { LoginErrorDialog(error = state.loginAction.error, onDismiss = {
state.loginAction.error.isWaitListError() -> { state.eventSink(LoginPasswordEvents.ClearError)
onWaitListError(state.formState) })
}
else -> {
LoginErrorDialog(error = state.loginAction.error, onDismiss = {
state.eventSink(LoginPasswordEvents.ClearError)
})
}
}
} }
} }
} }
@ -302,6 +293,5 @@ internal fun LoginPasswordViewPreview(@PreviewParameter(LoginPasswordStateProvid
LoginPasswordView( LoginPasswordView(
state = state, state = state,
onBackClick = {}, onBackClick = {},
onWaitListError = {},
) )
} }

View file

@ -1,14 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
sealed interface WaitListEvents {
data object AttemptLogin : WaitListEvents
data object ClearError : WaitListEvents
data object Continue : WaitListEvents
}

View file

@ -1,52 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
class WaitListNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: WaitListPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val loginFormState: LoginFormState) : NodeInputs
private val inputs: Inputs = inputs()
private val presenter = presenterFactory.create(inputs.loginFormState)
interface Callback : Plugin {
fun onCancelClick()
}
private fun onCancelClick() {
plugins<Callback>().forEach { it.onCancelClick() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
WaitListView(
state = state,
onCancelClick = ::onCancelClick,
modifier = modifier
)
}
}

View file

@ -1,87 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
class WaitListPresenter @AssistedInject constructor(
@Assisted private val formState: LoginFormState,
private val buildMeta: BuildMeta,
private val authenticationService: MatrixAuthenticationService,
private val defaultLoginUserStory: DefaultLoginUserStory,
) : Presenter<WaitListState> {
@AssistedFactory
interface Factory {
fun create(loginFormState: LoginFormState): WaitListPresenter
}
@Composable
override fun present(): WaitListState {
val coroutineScope = rememberCoroutineScope()
val homeserverUrl = remember {
authenticationService.getHomeserverDetails().value?.url ?: "server"
}
val loginAction: MutableState<AsyncData<SessionId>> = remember {
mutableStateOf(AsyncData.Uninitialized)
}
val attemptNumber = remember { mutableIntStateOf(0) }
fun handleEvents(event: WaitListEvents) {
when (event) {
WaitListEvents.AttemptLogin -> {
// Do not attempt to login on first resume of the View.
attemptNumber.intValue++
if (attemptNumber.intValue > 1) {
coroutineScope.loginAttempt(formState, loginAction)
}
}
WaitListEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
WaitListEvents.Continue -> defaultLoginUserStory.setLoginFlowIsDone(true)
}
}
return WaitListState(
appName = buildMeta.applicationName,
serverName = homeserverUrl,
loginAction = loginAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
Timber.w("Attempt to login...")
loggedInState.value = AsyncData.Loading()
authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId ->
loggedInState.value = AsyncData.Success(sessionId)
}
.onFailure { failure ->
loggedInState.value = AsyncData.Failure(failure)
}
}
}

View file

@ -1,19 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
// Do not use default value, so no member get forgotten in the presenters.
data class WaitListState(
val appName: String,
val serverName: String,
val loginAction: AsyncData<SessionId>,
val eventSink: (WaitListEvents) -> Unit
)

View file

@ -1,35 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
override val values: Sequence<WaitListState>
get() = sequenceOf(
aWaitListState(loginAction = AsyncData.Uninitialized),
aWaitListState(loginAction = AsyncData.Loading()),
aWaitListState(loginAction = AsyncData.Failure(Throwable("error"))),
aWaitListState(loginAction = AsyncData.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
aWaitListState(loginAction = AsyncData.Success(SessionId("@alice:element.io"))),
// Add other state here
)
}
fun aWaitListState(
appName: String = "Element X",
serverName: String = "server.org",
loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
) = WaitListState(
appName = appName,
serverName = serverName,
loginAction = loginAction,
eventSink = {}
)

View file

@ -1,143 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.isWaitListError
import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.ui.strings.CommonStrings
// Ref: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=6761-148425
// Only the first screen can be displayed, since once logged in, this Node will be remove by the RootNode.
@Composable
fun WaitListView(
state: WaitListState,
onCancelClick: () -> Unit,
modifier: Modifier = Modifier,
) {
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(WaitListEvents.AttemptLogin)
else -> Unit
}
}
WaitListContent(state, onCancelClick, modifier)
}
@Composable
private fun WaitListError(state: WaitListState) {
// Display a dialog for error other than the waitlist error
state.loginAction.errorOrNull()?.let { error ->
if (error.isWaitListError().not()) {
RetryDialog(
content = stringResource(id = loginError(error)),
onRetry = {
state.eventSink.invoke(WaitListEvents.AttemptLogin)
},
onDismiss = {
state.eventSink.invoke(WaitListEvents.ClearError)
}
)
}
}
}
@Composable
private fun WaitListContent(
state: WaitListState,
onCancelClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
) {
val title = stringResource(
when (state.loginAction) {
is AsyncData.Success -> R.string.screen_waitlist_title_success
else -> R.string.screen_waitlist_title
}
)
val subtitle = when (state.loginAction) {
is AsyncData.Success -> stringResource(
id = R.string.screen_waitlist_message_success,
state.appName,
)
else -> stringResource(
id = R.string.screen_waitlist_message,
state.appName,
state.serverName,
)
}
SunsetPage(
isLoading = state.loginAction.isLoading(),
title = title,
subtitle = subtitle,
) {
OverallContent(state, onCancelClick)
}
WaitListError(state)
}
}
@Composable
private fun OverallContent(
state: WaitListState,
onCancelClick: () -> Unit,
) {
Box(modifier = Modifier.fillMaxSize()) {
if (state.loginAction !is AsyncData.Success) {
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textOnSolidPrimary) {
TextButton(
text = stringResource(CommonStrings.action_cancel),
onClick = onCancelClick,
)
}
}
if (state.loginAction is AsyncData.Success) {
Button(
text = stringResource(id = CommonStrings.action_continue),
onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(bottom = 8.dp),
)
}
}
}
@PreviewsDayNight
@Composable
internal fun WaitListViewPreview(@PreviewParameter(WaitListStateProvider::class) state: WaitListState) = ElementPreview {
WaitListView(
state = state,
onCancelClick = {},
)
}

View file

@ -1,114 +0,0 @@
/*
* Copyright 2023, 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.login.impl.screens.waitlistscreen
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class WaitListPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val authenticationService = FakeMatrixAuthenticationService().apply {
givenHomeserver(A_HOMESERVER)
}
val loginUserStory = DefaultLoginUserStory()
val presenter = WaitListPresenter(
LoginFormState.Default,
aBuildMeta(applicationName = "Application Name"),
authenticationService,
loginUserStory,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo("Application Name")
assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL)
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
}
}
@Test
fun `present - attempt login with error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService().apply {
givenLoginError(A_THROWABLE)
}
val loginUserStory = DefaultLoginUserStory()
val presenter = WaitListPresenter(
LoginFormState.Default,
aBuildMeta(),
authenticationService,
loginUserStory,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// First usage of AttemptLogin, nothing should happen
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
expectNoEvents()
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val errorState = awaitItem()
assertThat(errorState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
// Assert the error can be cleared
errorState.eventSink(WaitListEvents.ClearError)
val clearedState = awaitItem()
assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
}
}
@Test
fun `present - attempt login with success`() = runTest {
val authenticationService = FakeMatrixAuthenticationService()
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
val presenter = WaitListPresenter(
LoginFormState.Default,
aBuildMeta(),
authenticationService,
loginUserStory,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
val initialState = awaitItem()
// First usage of AttemptLogin, nothing should happen
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
expectNoEvents()
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
val submitState = awaitItem()
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
val successState = awaitItem()
assertThat(successState.loginAction).isEqualTo(AsyncData.Success(A_USER_ID))
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
successState.eventSink.invoke(WaitListEvents.Continue)
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
}
}
}

View file

@ -16,6 +16,7 @@ interface LogoutEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder { interface NodeBuilder {
fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder
fun callback(callback: Callback): NodeBuilder fun callback(callback: Callback): NodeBuilder
fun build(): Node fun build(): Node
} }

View file

@ -27,6 +27,15 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint {
return this return this
} }
override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder {
plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin {
override fun onSuccessfulLogoutPendingAction() {
action()
}
}
return this
}
override fun build(): Node { override fun build(): Node {
return parentNode.createNode<LogoutNode>(buildContext, plugins) return parentNode.createNode<LogoutNode>(buildContext, plugins)
} }

View file

@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor(
plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() } plugins<LogoutEntryPoint.Callback>().forEach { it.onChangeRecoveryKeyClick() }
} }
interface SuccessfulLogoutPendingAction : Plugin {
fun onSuccessfulLogoutPendingAction()
}
private val customOnSuccessfulLogoutPendingAction = plugins<SuccessfulLogoutPendingAction>().firstOrNull()
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
@ -41,7 +47,10 @@ class LogoutNode @AssistedInject constructor(
LogoutView( LogoutView(
state = state, state = state,
onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick, onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
onSuccessLogout = { onSuccessLogout(activity, isDark, it) }, onSuccessLogout = {
customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction()
onSuccessLogout(activity, isDark, it)
},
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
modifier = modifier, modifier = modifier,
) )

View file

@ -18,11 +18,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import javax.inject.Inject import javax.inject.Inject
@ -32,7 +34,8 @@ class PinnedEventsTimelineProvider @Inject constructor(
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
) : TimelineProvider { ) : TimelineProvider {
private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> = MutableStateFlow(AsyncData.Uninitialized) private val _timelineStateFlow: MutableStateFlow<AsyncData<Timeline>> =
MutableStateFlow(AsyncData.Uninitialized)
override fun activeTimelineFlow(): StateFlow<Timeline?> { override fun activeTimelineFlow(): StateFlow<Timeline?> {
return _timelineStateFlow return _timelineStateFlow
@ -44,25 +47,46 @@ class PinnedEventsTimelineProvider @Inject constructor(
val timelineStateFlow = _timelineStateFlow val timelineStateFlow = _timelineStateFlow
fun launchIn(scope: CoroutineScope) { fun launchIn(scope: CoroutineScope) {
_timelineStateFlow.subscriptionCount
.map { count -> count > 0 }
.distinctUntilChanged()
.onEach { isActive ->
if (isActive) {
onActive()
} else {
onInactive()
}
}
.launchIn(scope)
}
private suspend fun onActive() = coroutineScope {
combine( combine(
featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents), featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents),
networkMonitor.connectivity networkMonitor.connectivity
) { ) { isEnabled, _ ->
// do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed // do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed
isEnabled, _ ->
isEnabled isEnabled
} }
.onEach { isFeatureEnabled -> .onEach { isFeatureEnabled ->
if (isFeatureEnabled) { if (isFeatureEnabled) {
loadTimelineIfNeeded() loadTimelineIfNeeded()
} else { } else {
_timelineStateFlow.value = AsyncData.Uninitialized resetTimeline()
} }
} }
.onCompletion { .launchIn(this)
invokeOnTimeline { close() } }
}
.launchIn(scope) private suspend fun onInactive() {
resetTimeline()
}
private suspend fun resetTimeline() {
invokeOnTimeline {
close()
}
_timelineStateFlow.emit(AsyncData.Uninitialized)
} }
suspend fun invokeOnTimeline(action: suspend Timeline.() -> Unit) { suspend fun invokeOnTimeline(action: suspend Timeline.() -> Unit) {

View file

@ -15,7 +15,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -60,6 +59,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private val timelineProvider: PinnedEventsTimelineProvider, private val timelineProvider: PinnedEventsTimelineProvider,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
actionListPresenterFactory: ActionListPresenter.Factory, actionListPresenterFactory: ActionListPresenter.Factory,
private val appCoroutineScope: CoroutineScope,
) : Presenter<PinnedMessagesListState> { ) : Presenter<PinnedMessagesListState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -100,10 +100,9 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
} }
) )
val coroutineScope = rememberCoroutineScope()
fun handleEvents(event: PinnedMessagesListEvents) { fun handleEvents(event: PinnedMessagesListEvents) {
when (event) { when (event) {
is PinnedMessagesListEvents.HandleAction -> coroutineScope.handleTimelineAction(event.action, event.event) is PinnedMessagesListEvents.HandleAction -> appCoroutineScope.handleTimelineAction(event.action, event.event)
} }
} }

View file

@ -16,14 +16,12 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemReadMarkerView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemReadMarkerView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
@ -40,7 +38,6 @@ fun TimelineItemVirtualRow(
when (virtual.model) { when (virtual.model) {
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model) is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model)
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView()
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name)
is TimelineItemLoadingIndicatorModel -> { is TimelineItemLoadingIndicatorModel -> {
TimelineLoadingMoreIndicator(virtual.model.direction) TimelineLoadingMoreIndicator(virtual.model.direction)

View file

@ -1,61 +0,0 @@
/*
* Copyright 2023, 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.messages.impl.timeline.components.virtual
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@Composable
fun TimelineEncryptedHistoryBannerView(
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp)
.clip(MaterialTheme.shapes.small)
.border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small)
.background(ElementTheme.colors.bgInfoSubtle)
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.InfoSolid(),
contentDescription = null,
tint = ElementTheme.colors.iconInfoPrimary
)
Text(
text = stringResource(R.string.screen_room_encrypted_history_banner),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textInfoPrimary
)
}
}
@PreviewsDayNight
@Composable
internal fun EncryptedHistoryBannerViewPreview() = ElementPreview {
TimelineEncryptedHistoryBannerView()
}

View file

@ -8,7 +8,6 @@
package io.element.android.features.messages.impl.timeline.factories.virtual package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
@ -34,7 +33,6 @@ class TimelineItemVirtualFactory @Inject constructor(
return when (val inner = virtual) { return when (val inner = virtual) {
is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner) is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner)
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel
is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel( is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel(
direction = inner.direction, direction = inner.direction,

View file

@ -1,12 +0,0 @@
/*
* Copyright 2023, 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.messages.impl.timeline.model.virtual
data object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel {
override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel"
}

View file

@ -35,11 +35,14 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class PinnedMessagesListPresenterTest { class PinnedMessagesListPresenterTest {
@Test @Test
fun `present - initial state feature disabled`() = runTest { fun `present - initial state feature disabled`() = runTest {
@ -155,6 +158,7 @@ class PinnedMessagesListPresenterTest {
val filledState = awaitItem() as PinnedMessagesListState.Filled val filledState = awaitItem() as PinnedMessagesListState.Filled
val eventItem = filledState.timelineItems.first() as TimelineItem.Event val eventItem = filledState.timelineItems.first() as TimelineItem.Event
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem)) filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem))
advanceUntilIdle()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
assert(redactEventLambda) assert(redactEventLambda)
.isCalledOnce() .isCalledOnce()
@ -184,9 +188,11 @@ class PinnedMessagesListPresenterTest {
pinnedEventsTimeline.unpinEventLambda = successUnpinEventLambda pinnedEventsTimeline.unpinEventLambda = successUnpinEventLambda
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem))
advanceUntilIdle()
pinnedEventsTimeline.unpinEventLambda = failureUnpinEventLambda pinnedEventsTimeline.unpinEventLambda = failureUnpinEventLambda
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem))
advanceUntilIdle()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -221,6 +227,7 @@ class PinnedMessagesListPresenterTest {
val filledState = awaitItem() as PinnedMessagesListState.Filled val filledState = awaitItem() as PinnedMessagesListState.Filled
val eventItem = filledState.timelineItems.first() as TimelineItem.Event val eventItem = filledState.timelineItems.first() as TimelineItem.Event
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewInTimeline, eventItem)) filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewInTimeline, eventItem))
advanceUntilIdle()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
assert(onViewInTimelineClickLambda) assert(onViewInTimelineClickLambda)
.isCalledOnce() .isCalledOnce()
@ -249,6 +256,7 @@ class PinnedMessagesListPresenterTest {
val filledState = awaitItem() as PinnedMessagesListState.Filled val filledState = awaitItem() as PinnedMessagesListState.Filled
val eventItem = filledState.timelineItems.first() as TimelineItem.Event val eventItem = filledState.timelineItems.first() as TimelineItem.Event
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewSource, eventItem)) filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewSource, eventItem))
advanceUntilIdle()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
assert(onShowEventDebugInfoClickLambda) assert(onShowEventDebugInfoClickLambda)
.isCalledOnce() .isCalledOnce()
@ -277,6 +285,7 @@ class PinnedMessagesListPresenterTest {
val filledState = awaitItem() as PinnedMessagesListState.Filled val filledState = awaitItem() as PinnedMessagesListState.Filled
val eventItem = filledState.timelineItems.first() as TimelineItem.Event val eventItem = filledState.timelineItems.first() as TimelineItem.Event
filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Forward, eventItem)) filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Forward, eventItem))
advanceUntilIdle()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
assert(onForwardEventClickLambda) assert(onForwardEventClickLambda)
.isCalledOnce() .isCalledOnce()
@ -322,6 +331,7 @@ class PinnedMessagesListPresenterTest {
timelineProvider = timelineProvider, timelineProvider = timelineProvider,
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory, actionListPresenterFactory = FakeActionListPresenter.Factory,
appCoroutineScope = this,
) )
} }
} }

View file

@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
fun onRoomSettingsClick(roomId: RoomId) fun onRoomSettingsClick(roomId: RoomId)
fun onReportBugClick() fun onReportBugClick()
fun onRoomDirectorySearchClick() fun onRoomDirectorySearchClick()
fun onLogoutForNativeSlidingSyncMigrationNeeded()
} }
} }

View file

@ -49,6 +49,7 @@ dependencies {
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.features.invite.api) implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api) implementation(projects.features.networkmonitor.api)
implementation(projects.features.logout.api)
implementation(projects.features.leaveroom.api) implementation(projects.features.leaveroom.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
@ -75,6 +76,7 @@ dependencies {
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.test) testImplementation(projects.features.leaveroom.test)
} }

View file

@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
aRoomsContentState(summaries = persistentListOf()), aRoomsContentState(summaries = persistentListOf()),
aSkeletonContentState(), aSkeletonContentState(),
anEmptyContentState(), anEmptyContentState(),
aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration),
) )
} }

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
sealed interface RoomListEvents { sealed interface RoomListEvents {
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
data object DismissRequestVerificationPrompt : RoomListEvents data object DismissRequestVerificationPrompt : RoomListEvents
data object DismissRecoveryKeyPrompt : RoomListEvents data object DismissBanner : RoomListEvents
data object ToggleSearchResults : RoomListEvents data object ToggleSearchResults : RoomListEvents
data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents

View file

@ -21,11 +21,14 @@ import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.invite.api.response.AcceptDeclineInviteView 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.api.RoomListEntryPoint
import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId 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 import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -36,6 +39,8 @@ class RoomListNode @AssistedInject constructor(
private val inviteFriendsUseCase: InviteFriendsUseCase, private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val acceptDeclineInviteView: AcceptDeclineInviteView, private val acceptDeclineInviteView: AcceptDeclineInviteView,
private val directLogoutView: DirectLogoutView,
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
init { init {
lifecycle.subscribe( lifecycle.subscribe(
@ -88,6 +93,7 @@ class RoomListNode @AssistedInject constructor(
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val activity = LocalContext.current as Activity val activity = LocalContext.current as Activity
RoomListView( RoomListView(
state = state, state = state,
onRoomClick = this::onRoomClick, onRoomClick = this::onRoomClick,
@ -98,6 +104,13 @@ class RoomListNode @AssistedInject constructor(
onRoomSettingsClick = this::onRoomSettingsClick, onRoomSettingsClick = this::onRoomSettingsClick,
onMenuActionClick = { onMenuActionClick(activity, it) }, onMenuActionClick = { onMenuActionClick(activity, it) },
onRoomDirectorySearchClick = this::onRoomDirectorySearchClick, onRoomDirectorySearchClick = this::onRoomDirectorySearchClick,
onMigrateToNativeSlidingSyncClick = {
if (state.directLogoutState.canDoDirectSignOut) {
state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
} else {
plugins<RoomListEntryPoint.Callback>().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() }
}
},
modifier = modifier, modifier = modifier,
) { ) {
acceptDeclineInviteView.Render( acceptDeclineInviteView.Render(
@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor(
modifier = Modifier modifier = Modifier
) )
} }
directLogoutView.Render(state.directLogoutState) {
enableNativeSlidingSyncUseCase()
}
} }
} }

View file

@ -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.invite.api.response.InviteData
import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter 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.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter,
private val notificationCleaner: NotificationCleaner, private val notificationCleaner: NotificationCleaner,
private val logoutPresenter: DirectLogoutPresenter,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService() private val encryptionService: EncryptionService = client.encryptionService()
private val syncService: SyncService = client.syncService() private val syncService: SyncService = client.syncService()
@ -115,13 +117,15 @@ class RoomListPresenter @Inject constructor(
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) } val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
val directLogoutState = logoutPresenter.present()
fun handleEvents(event: RoomListEvents) { fun handleEvents(event: RoomListEvents) {
when (event) { when (event) {
is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch { is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch {
updateVisibleRange(event.range) updateVisibleRange(event.range)
} }
RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true
RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true RoomListEvents.DismissBanner -> securityBannerDismissed = true
RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility)
is RoomListEvents.ShowContextMenu -> { is RoomListEvents.ShowContextMenu -> {
coroutineScope.showContextMenu(event, contextMenu) coroutineScope.showContextMenu(event, contextMenu)
@ -161,6 +165,7 @@ class RoomListPresenter @Inject constructor(
searchState = searchState, searchState = searchState,
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
directLogoutState = directLogoutState,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
@ -168,6 +173,7 @@ class RoomListPresenter @Inject constructor(
@Composable @Composable
private fun securityBannerState( private fun securityBannerState(
securityBannerDismissed: Boolean, securityBannerDismissed: Boolean,
needsSlidingSyncMigration: Boolean,
): State<SecurityBannerState> { ): State<SecurityBannerState> {
val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed) val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed)
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
@ -185,6 +191,7 @@ class RoomListPresenter @Inject constructor(
RecoveryState.ENABLED -> SecurityBannerState.None RecoveryState.ENABLED -> SecurityBannerState.None
} }
} }
needsSlidingSyncMigration -> SecurityBannerState.NeedsNativeSlidingSyncMigration
else -> SecurityBannerState.None else -> SecurityBannerState.None
} }
} }
@ -209,11 +216,14 @@ class RoomListPresenter @Inject constructor(
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
} }
} }
val needsSlidingSyncMigration by produceState(false) {
value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
}
return when { return when {
showEmpty -> RoomListContentState.Empty showEmpty -> RoomListContentState.Empty
showSkeleton -> RoomListContentState.Skeleton(count = 16) showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> { else -> {
val securityBannerState by securityBannerState(securityBannerDismissed) val securityBannerState by securityBannerState(securityBannerDismissed, needsSlidingSyncMigration)
RoomListContentState.Rooms( RoomListContentState.Rooms(
securityBannerState = securityBannerState, securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),

View file

@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState 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.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
@ -31,6 +32,7 @@ data class RoomListState(
val searchState: RoomListSearchState, val searchState: RoomListSearchState,
val contentState: RoomListContentState, val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val directLogoutState: DirectLogoutState,
val eventSink: (RoomListEvents) -> Unit, val eventSink: (RoomListEvents) -> Unit,
) { ) {
val displayFilters = contentState is RoomListContentState.Rooms val displayFilters = contentState is RoomListContentState.Rooms
@ -59,6 +61,7 @@ enum class SecurityBannerState {
None, None,
SetUpRecovery, SetUpRecovery,
RecoveryKeyConfirmation, RecoveryKeyConfirmation,
NeedsNativeSlidingSyncMigration,
} }
@Immutable @Immutable

View file

@ -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.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState 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.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
@ -57,6 +59,7 @@ internal fun aRoomListState(
filtersState: RoomListFiltersState = aRoomListFiltersState(), filtersState: RoomListFiltersState = aRoomListFiltersState(),
contentState: RoomListContentState = aRoomsContentState(), contentState: RoomListContentState = aRoomsContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (RoomListEvents) -> Unit = {} eventSink: (RoomListEvents) -> Unit = {}
) = RoomListState( ) = RoomListState(
matrixUser = matrixUser, matrixUser = matrixUser,
@ -69,6 +72,7 @@ internal fun aRoomListState(
searchState = searchState, searchState = searchState,
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
directLogoutState = directLogoutState,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -50,6 +50,7 @@ fun RoomListView(
onRoomSettingsClick: (roomId: RoomId) -> Unit, onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit,
onRoomDirectorySearchClick: () -> Unit, onRoomDirectorySearchClick: () -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit, acceptDeclineInviteView: @Composable () -> Unit,
) { ) {
@ -76,6 +77,7 @@ fun RoomListView(
onOpenSettings = onSettingsClick, onOpenSettings = onSettingsClick,
onCreateRoomClick = onCreateRoomClick, onCreateRoomClick = onCreateRoomClick,
onMenuActionClick = onMenuActionClick, onMenuActionClick = onMenuActionClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
modifier = Modifier.padding(top = topPadding), modifier = Modifier.padding(top = topPadding),
) )
// This overlaid view will only be visible when state.displaySearchResults is true // This overlaid view will only be visible when state.displaySearchResults is true
@ -105,6 +107,7 @@ private fun RoomListScaffold(
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onCreateRoomClick: () -> Unit, onCreateRoomClick: () -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun onRoomClick(room: RoomListRoomSummary) { fun onRoomClick(room: RoomListRoomSummary) {
@ -140,6 +143,7 @@ private fun RoomListScaffold(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick, onRoomClick = ::onRoomClick,
onCreateRoomClick = onCreateRoomClick, onCreateRoomClick = onCreateRoomClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
@ -180,5 +184,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
onMenuActionClick = {}, onMenuActionClick = {},
onRoomDirectorySearchClick = {}, onRoomDirectorySearchClick = {},
acceptDeclineInviteView = {}, acceptDeclineInviteView = {},
onMigrateToNativeSlidingSyncClick = {},
) )
} }

View file

@ -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 = {},
)
}

View file

@ -64,6 +64,7 @@ fun RoomListContentView(
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit, onCreateRoomClick: () -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
@ -85,6 +86,7 @@ fun RoomListContentView(
eventSink = eventSink, eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
) )
} }
@ -133,6 +135,7 @@ private fun RoomsView(
onSetUpRecoveryClick: () -> Unit, onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@ -147,6 +150,7 @@ private fun RoomsView(
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} }
@ -159,6 +163,7 @@ private fun RoomsViewList(
onSetUpRecoveryClick: () -> Unit, onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
onMigrateToNativeSlidingSyncClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -185,7 +190,7 @@ private fun RoomsViewList(
item { item {
SetUpRecoveryKeyBanner( SetUpRecoveryKeyBanner(
onContinueClick = onSetUpRecoveryClick, onContinueClick = onSetUpRecoveryClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) } onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
) )
} }
} }
@ -193,7 +198,15 @@ private fun RoomsViewList(
item { item {
ConfirmRecoveryKeyBanner( ConfirmRecoveryKeyBanner(
onContinueClick = onConfirmRecoveryKeyClick, 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 = {}, onConfirmRecoveryKeyClick = {},
onRoomClick = {}, onRoomClick = {},
onCreateRoomClick = {}, onCreateRoomClick = {},
onMigrateToNativeSlidingSyncClick = {},
) )
} }

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Log Out &amp; 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_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="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> <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>

View file

@ -7,6 +7,7 @@
package io.element.android.features.roomlist.impl package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
@ -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.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter 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.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
@ -240,7 +243,7 @@ class RoomListPresenterTest {
sessionVerificationService = FakeSessionVerificationService().apply { sessionVerificationService = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false) givenNeedsSessionVerification(false)
}, },
syncService = FakeSyncService(MutableStateFlow(SyncState.Running)) syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
) )
val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
@ -268,7 +271,7 @@ class RoomListPresenterTest {
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
encryptionService.emitRecoveryState(RecoveryState.DISABLED) encryptionService.emitRecoveryState(RecoveryState.DISABLED)
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery)
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) nextState.eventSink(RoomListEvents.DismissBanner)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel() scope.cancel()
@ -644,6 +647,10 @@ class RoomListPresenterTest {
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() }, searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter {
@Composable
override fun present() = aDirectLogoutState()
},
) = RoomListPresenter( ) = RoomListPresenter(
client = client, client = client,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
@ -671,5 +678,6 @@ class RoomListPresenterTest {
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(), fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(),
notificationCleaner = notificationCleaner, notificationCleaner = notificationCleaner,
logoutPresenter = logoutPresenter,
) )
} }

View file

@ -68,7 +68,7 @@ class RoomListViewTest {
val close = rule.activity.getString(CommonStrings.action_close) val close = rule.activity.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick() rule.onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt) eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
} }
@Test @Test
@ -86,7 +86,7 @@ class RoomListViewTest {
val close = rule.activity.getString(CommonStrings.action_close) val close = rule.activity.getString(CommonStrings.action_close)
rule.onNodeWithContentDescription(close).performClick() rule.onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt) eventsRecorder.assertSingle(RoomListEvents.DismissBanner)
} }
@Test @Test
@ -232,6 +232,21 @@ class RoomListViewTest {
listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)), 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( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListView(
@ -244,6 +259,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(), onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled()
) { ) {
setContent { setContent {
RoomListView( RoomListView(
@ -256,6 +272,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onRoomSettingsClick = onRoomSettingsClick, onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick, onMenuActionClick = onMenuActionClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick, onRoomDirectorySearchClick = onRoomDirectorySearchClick,
onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick,
acceptDeclineInviteView = { }, acceptDeclineInviteView = { },
) )
} }

View file

@ -39,6 +39,7 @@ fun DialogLikeBannerMolecule(
onSubmitClick: () -> Unit, onSubmitClick: () -> Unit,
onDismissClick: (() -> Unit)?, onDismissClick: (() -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
actionText: String = stringResource(CommonStrings.action_continue),
) { ) {
Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Surface( Surface(
@ -74,7 +75,7 @@ fun DialogLikeBannerMolecule(
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Button( Button(
text = stringResource(CommonStrings.action_continue), text = actionText,
size = ButtonSize.Medium, size = ButtonSize.Medium,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = onSubmitClick, onClick = onSubmitClick,

View file

@ -126,4 +126,10 @@ interface MatrixClient : Closeable {
*/ */
suspend fun getUrl(url: String): Result<String> suspend fun getUrl(url: String): Result<String>
suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomPreview> 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
} }

View file

@ -16,8 +16,6 @@ sealed interface VirtualTimelineItem {
data object ReadMarker : VirtualTimelineItem data object ReadMarker : VirtualTimelineItem
data object EncryptedHistoryBanner : VirtualTimelineItem
data object RoomBeginning : VirtualTimelineItem data object RoomBeginning : VirtualTimelineItem
data object LastForwardIndicator : VirtualTimelineItem data object LastForwardIndicator : VirtualTimelineItem

View file

@ -87,13 +87,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.BackupState
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.ClientException
import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
@ -179,7 +179,6 @@ class RustMatrixClient(
systemClock = clock, systemClock = clock,
roomContentForwarder = RoomContentForwarder(innerRoomListService), roomContentForwarder = RoomContentForwarder(innerRoomListService),
roomSyncSubscriber = roomSyncSubscriber, roomSyncSubscriber = roomSyncSubscriber,
isKeyBackupEnabled = { client.encryption().use { it.backupState() == BackupState.ENABLED } },
getSessionData = { sessionStore.getSession(sessionId.value)!! }, getSessionData = { sessionStore.getSession(sessionId.value)!! },
) )
@ -530,6 +529,14 @@ class RustMatrixClient(
}) })
}.buffer(Channel.UNLIMITED) }.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) { internal fun setDelegate(delegate: RustClientSessionDelegate) {
client.setDelegate(delegate) client.setDelegate(delegate)
} }

View file

@ -89,7 +89,6 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixRoom( class RustMatrixRoom(
override val sessionId: SessionId, override val sessionId: SessionId,
private val isKeyBackupEnabled: Boolean,
private val roomListItem: RoomListItem, private val roomListItem: RoomListItem,
private val innerRoom: InnerRoom, private val innerRoom: InnerRoom,
innerTimeline: InnerTimeline, innerTimeline: InnerTimeline,
@ -652,16 +651,14 @@ class RustMatrixRoom(
): Timeline { ): Timeline {
val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline") val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline")
return RustTimeline( return RustTimeline(
isKeyBackupEnabled = isKeyBackupEnabled,
mode = mode, mode = mode,
matrixRoom = this, matrixRoom = this,
inner = timeline,
systemClock = systemClock, systemClock = systemClock,
coroutineScope = timelineCoroutineScope, coroutineScope = timelineCoroutineScope,
dispatcher = roomDispatcher, dispatcher = roomDispatcher,
lastLoginTimestamp = sessionData.loginTimestamp,
onNewSyncedEvent = onNewSyncedEvent,
roomContentForwarder = roomContentForwarder, roomContentForwarder = roomContentForwarder,
inner = timeline, onNewSyncedEvent = onNewSyncedEvent,
) )
} }
} }

View file

@ -46,7 +46,6 @@ class RustRoomFactory(
private val roomListService: RoomListService, private val roomListService: RoomListService,
private val innerRoomListService: InnerRoomListService, private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
private val isKeyBackupEnabled: suspend () -> Boolean,
private val getSessionData: suspend () -> SessionData, private val getSessionData: suspend () -> SessionData,
) { ) {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -109,7 +108,6 @@ class RustRoomFactory(
val liveTimeline = roomReferences.fullRoom.timeline() val liveTimeline = roomReferences.fullRoom.timeline()
RustMatrixRoom( RustMatrixRoom(
sessionId = sessionId, sessionId = sessionId,
isKeyBackupEnabled = isKeyBackupEnabled(),
roomListItem = roomReferences.roomListItem, roomListItem = roomReferences.roomListItem,
innerRoom = roomReferences.fullRoom, innerRoom = roomReferences.fullRoom,
innerTimeline = liveTimeline, innerTimeline = liveTimeline,

View file

@ -40,7 +40,6 @@ import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTim
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper
import io.element.android.libraries.matrix.impl.util.MessageEventContent import io.element.android.libraries.matrix.impl.util.MessageEventContent
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
@ -70,7 +69,6 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
import java.io.File import java.io.File
import java.util.Date
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
private const val PAGINATION_SIZE = 50 private const val PAGINATION_SIZE = 50
@ -79,11 +77,9 @@ class RustTimeline(
private val inner: InnerTimeline, private val inner: InnerTimeline,
mode: Timeline.Mode, mode: Timeline.Mode,
systemClock: SystemClock, systemClock: SystemClock,
isKeyBackupEnabled: Boolean,
private val matrixRoom: MatrixRoom, private val matrixRoom: MatrixRoom,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
lastLoginTimestamp: Date?,
private val roomContentForwarder: RoomContentForwarder, private val roomContentForwarder: RoomContentForwarder,
onNewSyncedEvent: () -> Unit, onNewSyncedEvent: () -> Unit,
) : Timeline { ) : Timeline {
@ -107,12 +103,6 @@ class RustTimeline(
timelineItems = _timelineItems, timelineItems = _timelineItems,
timelineItemFactory = timelineItemMapper, timelineItemFactory = timelineItemMapper,
) )
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(
lastLoginTimestamp = lastLoginTimestamp,
isRoomEncrypted = matrixRoom.isEncrypted,
isKeyBackupEnabled = isKeyBackupEnabled,
dispatcher = dispatcher,
)
private val timelineItemsSubscriber = TimelineItemsSubscriber( private val timelineItemsSubscriber = TimelineItemsSubscriber(
timeline = inner, timeline = inner,
timelineCoroutineScope = coroutineScope, timelineCoroutineScope = coroutineScope,
@ -219,7 +209,6 @@ class RustTimeline(
) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward, isInit -> ) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward, isInit ->
withContext(dispatcher) { withContext(dispatcher) {
timelineItems timelineItems
.process { items -> encryptedHistoryPostProcessor.process(items) }
.process { items -> .process { items ->
roomBeginningPostProcessor.process( roomBeginningPostProcessor.process(
items = items, items = items,

View file

@ -1,17 +0,0 @@
/*
* 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.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
internal fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
val firstItem = firstOrNull()
return firstItem is MatrixTimelineItem.Virtual &&
firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
}

View file

@ -19,11 +19,10 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
hasMoreToLoadBackward: Boolean, hasMoreToLoadBackward: Boolean,
hasMoreToLoadForward: Boolean, hasMoreToLoadForward: Boolean,
): List<MatrixTimelineItem> { ): List<MatrixTimelineItem> {
val shouldAddBackwardLoadingIndicator = hasMoreToLoadBackward && !items.hasEncryptionHistoryBanner()
val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty() val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
val currentTimestamp = systemClock.epochMillis() val currentTimestamp = systemClock.epochMillis()
return buildList { return buildList {
if (shouldAddBackwardLoadingIndicator) { if (hasMoreToLoadBackward) {
val backwardLoadingIndicator = MatrixTimelineItem.Virtual( val backwardLoadingIndicator = MatrixTimelineItem.Virtual(
uniqueId = UniqueId("BackwardLoadingIndicator"), uniqueId = UniqueId("BackwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator( virtual = VirtualTimelineItem.LoadingIndicator(

View file

@ -36,7 +36,6 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
} }
private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> { private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
if (items.hasEncryptionHistoryBanner()) return items
val roomBeginningItem = createRoomBeginningItem() val roomBeginningItem = createRoomBeginningItem()
return listOf(roomBeginningItem) + items return listOf(roomBeginningItem) + items
} }

View file

@ -1,55 +0,0 @@
/*
* Copyright 2023, 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.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Date
internal val encryptedHistoryBannerId = UniqueId("EncryptedHistoryBannerId")
class TimelineEncryptedHistoryPostProcessor(
private val dispatcher: CoroutineDispatcher,
private val lastLoginTimestamp: Date?,
private val isRoomEncrypted: Boolean,
private val isKeyBackupEnabled: Boolean,
) {
suspend fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = withContext(dispatcher) {
Timber.d("Process on Thread=${Thread.currentThread()}")
if (!isRoomEncrypted || isKeyBackupEnabled || lastLoginTimestamp == null) return@withContext items
replaceWithEncryptionHistoryBannerIfNeeded(items)
}
private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
var lastEncryptedHistoryBannerIndex = -1
for ((i, item) in list.withIndex()) {
if (isItemEncryptionHistory(item)) {
lastEncryptedHistoryBannerIndex = i
}
}
return if (lastEncryptedHistoryBannerIndex >= 0) {
val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList()
sublist.add(0, MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))
sublist
} else {
list
}
}
private fun isItemEncryptionHistory(item: MatrixTimelineItem): Boolean {
if ((item as? MatrixTimelineItem.Virtual)?.virtual is VirtualTimelineItem.EncryptedHistoryBanner) {
return true
}
val timestamp = (item as? MatrixTimelineItem.Event)?.event?.timestamp ?: return false
return timestamp <= lastLoginTimestamp!!.time
}
}

View file

@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.aMessageContent
@ -70,16 +69,6 @@ class RoomBeginningPostProcessorTest {
) )
} }
@Test
fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() {
val timelineItems = listOf(
MatrixTimelineItem.Virtual(UniqueId("EncryptedHistoryBanner"), VirtualTimelineItem.EncryptedHistoryBanner),
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
assertThat(processedItems).isEqualTo(timelineItems)
}
@Test @Test
fun `processor won't remove items if it's not at the start of the timeline`() { fun `processor won't remove items if it's not at the start of the timeline`() {
val timelineItems = listOf( val timelineItems = listOf(

View file

@ -1,114 +0,0 @@
/*
* Copyright 2023, 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.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.Date
class TimelineEncryptedHistoryPostProcessorTest {
private val defaultLastLoginTimestamp = Date(1_689_061_264L)
@Test
fun `given an unencrypted room, nothing is done`() = runTest {
val processor = createPostProcessor(isRoomEncrypted = false)
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given an encrypted room, and key backup enabled, nothing is done`() = runTest {
val processor = createPostProcessor(isKeyBackupEnabled = true)
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given a null lastLoginTimestamp, nothing is done`() = runTest {
val processor = createPostProcessor(lastLoginTimestamp = null)
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given an empty list, nothing is done`() = runTest {
val processor = createPostProcessor()
val items = emptyList<MatrixTimelineItem>()
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
)
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
)
assertThat(processor.process(items))
.isEqualTo(listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner)))
}
@Test
fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
)
assertThat(processor.process(items)).isEqualTo(
listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))
)
}
@Test
fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, then they're replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
)
assertThat(processor.process(items)).isEqualTo(
listOf(
MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner),
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
)
)
}
private fun TestScope.createPostProcessor(
lastLoginTimestamp: Date? = defaultLastLoginTimestamp,
isRoomEncrypted: Boolean = true,
isKeyBackupEnabled: Boolean = false,
) = TimelineEncryptedHistoryPostProcessor(
lastLoginTimestamp = lastLoginTimestamp,
isRoomEncrypted = isRoomEncrypted,
isKeyBackupEnabled = isKeyBackupEnabled,
dispatcher = StandardTestDispatcher(testScheduler)
)
}

View file

@ -77,6 +77,8 @@ class FakeMatrixClient(
private val clearCacheLambda: () -> Unit = { lambdaError() }, private val clearCacheLambda: () -> Unit = { lambdaError() },
private val userIdServerNameLambda: () -> String = { lambdaError() }, private val userIdServerNameLambda: () -> String = { lambdaError() },
private val getUrlLambda: (String) -> Result<String> = { lambdaError() }, private val getUrlLambda: (String) -> Result<String> = { lambdaError() },
var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true },
var isUsingNativeSlidingSyncLambda: () -> Boolean = { true },
) : MatrixClient { ) : MatrixClient {
var setDisplayNameCalled: Boolean = false var setDisplayNameCalled: Boolean = false
private set private set
@ -316,4 +318,12 @@ class FakeMatrixClient(
override suspend fun getUrl(url: String): Result<String> { override suspend fun getUrl(url: String): Result<String> {
return getUrlLambda(url) return getUrlLambda(url)
} }
override suspend fun isNativeSlidingSyncSupported(): Boolean {
return isNativeSlidingSyncSupportedLambda()
}
override fun isUsingNativeSlidingSync(): Boolean {
return isUsingNativeSlidingSyncLambda()
}
} }

View file

@ -17,4 +17,8 @@ dependencies {
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
testImplementation(projects.libraries.preferences.test)
testImplementation(libs.test.truth)
testImplementation(libs.coroutines.test)
} }

View file

@ -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)
}
}
}

View file

@ -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()
}
}

View file

@ -47,7 +47,7 @@ private const val versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release. // Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
private const val versionPatch = 3 private const val versionPatch = 4
object Versions { object Versions {
val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch

View file

@ -56,6 +56,7 @@ dependencies {
implementation(projects.features.roomlist.impl) implementation(projects.features.roomlist.impl)
implementation(projects.features.leaveroom.impl) implementation(projects.features.leaveroom.impl)
implementation(projects.features.login.impl) implementation(projects.features.login.impl)
implementation(projects.features.logout.impl)
implementation(projects.features.networkmonitor.impl) implementation(projects.features.networkmonitor.impl)
implementation(projects.services.toolbox.impl) implementation(projects.services.toolbox.impl)
implementation(projects.libraries.featureflag.impl) implementation(projects.libraries.featureflag.impl)

View file

@ -38,7 +38,6 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
state = state, state = state,
modifier = modifier, modifier = modifier,
onBackClick = {}, onBackClick = {},
onWaitListError = {},
) )
} }
} }

View file

@ -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.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter 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.networkmonitor.impl.DefaultNetworkMonitor
import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListView import io.element.android.features.roomlist.impl.RoomListView
@ -144,6 +145,7 @@ class RoomListScreen(
} }
}, },
notificationCleaner = FakeNotificationCleaner(), notificationCleaner = FakeNotificationCleaner(),
logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService),
) )
@Composable @Composable
@ -172,7 +174,8 @@ class RoomListScreen(
modifier = modifier, modifier = modifier,
acceptDeclineInviteView = { acceptDeclineInviteView = {
AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {}) AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {})
} },
onMigrateToNativeSlidingSyncClick = {},
) )
DisposableEffect(Unit) { DisposableEffect(Unit) {

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2c858bc7e2beecc0d4c92df0b4ac61e1e3a975a072e0e75cfa1da6aaa32142c
size 140926

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8524bdd37ac6eb784d82041b572d9cf3bb69cf3de318ba1d8abc45e9a239dad1
size 141726

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4fd2455de9a763e582e4977e36686c714dfb380737fc6193b4eab9ef8b64de9
size 58641

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2c858bc7e2beecc0d4c92df0b4ac61e1e3a975a072e0e75cfa1da6aaa32142c
size 140926

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7880a67dd818c44b519be634a4a33a4a0efc842f9ad5e4879f16acb7c8b1f5f
size 122622

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa9162336cb7d46a2517f4b4149a9f8389e69e410e3896cb6b973fc16a940adf
size 138058

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef852707489db29300b4bf8259a5d71d3c82ce26bcea3ddb00d037b1ba379423
size 138901

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5946208785bcc7eff5e76048cd467e9bde6f4ff5874ef2dd2562d5bbb0844f93
size 59177

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa9162336cb7d46a2517f4b4149a9f8389e69e410e3896cb6b973fc16a940adf
size 138058

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3b4fe8418f426d9953189908053407550ad6d83d45a548253d75507321f686c9
size 120721

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d81ad9f5a5983653d9266a2500b2c71dc7ca231d52a62d1c1f1b091849ede32
size 165501

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1cb4f2cbdc2b7fca643a9debff50206d4aa9321894a86149d6c815fd4db7b593
size 166084

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3b942ce79745a26aa2d4227c35aaad442b35c2f768915e3d9fabf45a04cd1d82
size 61267

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d81ad9f5a5983653d9266a2500b2c71dc7ca231d52a62d1c1f1b091849ede32
size 165501

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa47e394a5af6cad06496a5d36fa81b2e7d76dd9eaec45c3914f1ffce45f9f91
size 146617

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47c6681e3cf56d3229953b7c34b6f1d531c492d23f4dffe77e2a12fbdbf44261
size 11892

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:985b87b568e34b16d132a5cde5781e99edc81be4ef53c5b6e67ca75472266f99
size 11436

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45f8b8767d1a54afde878f1c853a7fd6dd5823d04db3b75a7c18c5d6fc742d8e
size 36110

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6849a6c73341edc3f37fd6ef968c07e63277d18f833028386012fa2a438ade7e
size 34946

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f4ee87f2ae823b3d026b70d6b91463ba6aa3ad7e454b915c0eba3901b219169b
size 72188

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:863e7f66501f357728aa048560dc59fccd63bff8fc4b648296d82db119b0137d
size 71031

View file

@ -139,6 +139,7 @@
"session_verification_banner_.*", "session_verification_banner_.*",
"confirm_recovery_key_banner_.*", "confirm_recovery_key_banner_.*",
"banner\\.set_up_recovery\\..*", "banner\\.set_up_recovery\\..*",
"banner\\.migrate_to_native_sliding_sync\\..*",
"full_screen_intent_banner_.*", "full_screen_intent_banner_.*",
"screen_migration_.*", "screen_migration_.*",
"screen_invites_.*" "screen_invites_.*"

View file

@ -177,9 +177,8 @@ printf "Committing...\n"
git commit -a -m 'version++' git commit -a -m 'version++'
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "Wait for the GitHub action https://github.com/element-hq/element-x-android/actions/workflows/release.yml?query=branch%%3Amain to build the 'main' branch.\n" printf "The GitHub action https://github.com/element-hq/element-x-android/actions/workflows/release.yml?query=branch%%3Amain should have start a new run.\n"
printf "Please enter the url of the github action (!!! WARNING: NOT THE URL OF THE ARTIFACT ANYMORE !!!)\n" read -p "Please enter the url of the run, no need to wait for it to complete (example: https://github.com/element-hq/element-x-android/actions/runs/9065756777): " runUrl
read -p "For instance https://github.com/element-hq/element-x-android/actions/runs/9065756777: " runUrl
targetPath="./tmp/Element/${version}" targetPath="./tmp/Element/${version}"
@ -270,7 +269,7 @@ printf "File app-fdroid-x86_64-release-signed.apk:\n"
"${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk | grep package "${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk | grep package
printf "\n" printf "\n"
read -p "Does it look correct? Press enter when it's done." read -p "Does it look correct? Press enter when it's done. "
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "The APKs in ${fdroidTargetPath} have been signed!\n" printf "The APKs in ${fdroidTargetPath} have been signed!\n"
@ -363,7 +362,7 @@ read -p ". Press enter to continue. "
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "Update the project release notes:\n\n" printf "Update the project release notes:\n\n"
read -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. \n" read -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. "
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "Committing...\n" printf "Committing...\n"