Move migration screen to within the room list (#2361)

* Rename migration bg drawable and add night variant
* Move `migration` package from `ftue` to `messages:impl` module
* Update `SunsetPage` with light and dark modes
* Fix bloom colors when nested theme is used
* Integrate the migration screen in the room list
* Fix `WaitListView` cancel button color
* Clear migration store when removing the app's cache

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-02-08 11:01:08 +01:00 committed by GitHub
parent 52dfaea741
commit d06e5c23cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 274 additions and 291 deletions

1
changelog.d/2310.misc Normal file
View file

@ -0,0 +1 @@
Move migration screen to within the room list

View file

@ -33,7 +33,6 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.DefaultFtueState
import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.state.FtueStep
@ -74,9 +73,6 @@ class FtueFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object Placeholder : NavTarget data object Placeholder : NavTarget
@Parcelize
data object MigrationScreen : NavTarget
@Parcelize @Parcelize
data object WelcomeScreen : NavTarget data object WelcomeScreen : NavTarget
@ -114,14 +110,6 @@ class FtueFlowNode @AssistedInject constructor(
NavTarget.Placeholder -> { NavTarget.Placeholder -> {
createNode<PlaceholderNode>(buildContext) createNode<PlaceholderNode>(buildContext)
} }
NavTarget.MigrationScreen -> {
val callback = object : MigrationScreenNode.Callback {
override fun onMigrationFinished() {
lifecycleScope.launch { moveToNextStep() }
}
}
createNode<MigrationScreenNode>(buildContext, listOf(callback))
}
NavTarget.WelcomeScreen -> { NavTarget.WelcomeScreen -> {
val callback = object : WelcomeNode.Callback { val callback = object : WelcomeNode.Callback {
override fun onContinueClicked() { override fun onContinueClicked() {
@ -158,9 +146,6 @@ class FtueFlowNode @AssistedInject constructor(
private fun moveToNextStep() { private fun moveToNextStep() {
when (ftueState.getNextStep()) { when (ftueState.getNextStep()) {
FtueStep.MigrationScreen -> {
backstack.newRoot(NavTarget.MigrationScreen)
}
FtueStep.WelcomeScreen -> { FtueStep.WelcomeScreen -> {
backstack.newRoot(NavTarget.WelcomeScreen) backstack.newRoot(NavTarget.WelcomeScreen)
} }

View file

@ -1,52 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.ftue.impl.migration
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 dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class MigrationScreenNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: MigrationScreenPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onMigrationFinished()
}
private fun onMigrationFinished() {
plugins.filterIsInstance<Callback>().forEach { it.onMigrationFinished() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
MigrationScreenView(
state,
onMigrationFinished = ::onMigrationFinished,
modifier = modifier
)
}
}

View file

@ -21,11 +21,9 @@ import android.os.Build
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ -43,17 +41,14 @@ class DefaultFtueState @Inject constructor(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val welcomeScreenState: WelcomeScreenState, private val welcomeScreenState: WelcomeScreenState,
private val migrationScreenStore: MigrationScreenStore,
private val permissionStateProvider: PermissionStateProvider, private val permissionStateProvider: PermissionStateProvider,
private val lockScreenService: LockScreenService, private val lockScreenService: LockScreenService,
private val matrixClient: MatrixClient,
) : FtueState { ) : FtueState {
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete()) override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
override suspend fun reset() { override suspend fun reset() {
welcomeScreenState.reset() welcomeScreenState.reset()
analyticsService.reset() analyticsService.reset()
migrationScreenStore.reset()
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS) permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
} }
@ -67,12 +62,7 @@ class DefaultFtueState @Inject constructor(
fun getNextStep(currentStep: FtueStep? = null): FtueStep? = fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
when (currentStep) { when (currentStep) {
null -> if (shouldDisplayMigrationScreen()) { null -> if (shouldDisplayWelcomeScreen()) {
FtueStep.MigrationScreen
} else {
getNextStep(FtueStep.MigrationScreen)
}
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) {
FtueStep.WelcomeScreen FtueStep.WelcomeScreen
} else { } else {
getNextStep(FtueStep.WelcomeScreen) getNextStep(FtueStep.WelcomeScreen)
@ -97,7 +87,6 @@ class DefaultFtueState @Inject constructor(
private fun isAnyStepIncomplete(): Boolean { private fun isAnyStepIncomplete(): Boolean {
return listOf( return listOf(
{ shouldDisplayMigrationScreen() },
{ shouldDisplayWelcomeScreen() }, { shouldDisplayWelcomeScreen() },
{ shouldAskNotificationPermissions() }, { shouldAskNotificationPermissions() },
{ needsAnalyticsOptIn() }, { needsAnalyticsOptIn() },
@ -105,10 +94,6 @@ class DefaultFtueState @Inject constructor(
).any { it() } ).any { it() }
} }
private fun shouldDisplayMigrationScreen(): Boolean {
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
}
private fun needsAnalyticsOptIn(): Boolean { private fun needsAnalyticsOptIn(): Boolean {
// We need this function to not be suspend, so we need to load the value through runBlocking // We need this function to not be suspend, so we need to load the value through runBlocking
return runBlocking { analyticsService.didAskUserConsent().first().not() } return runBlocking { analyticsService.didAskUserConsent().first().not() }
@ -147,7 +132,6 @@ class DefaultFtueState @Inject constructor(
} }
sealed interface FtueStep { sealed interface FtueStep {
data object MigrationScreen : FtueStep
data object WelcomeScreen : FtueStep data object WelcomeScreen : FtueStep
data object NotificationsOptIn : FtueStep data object NotificationsOptIn : FtueStep
data object AnalyticsOptIn : FtueStep data object AnalyticsOptIn : FtueStep

View file

@ -1,7 +1,5 @@
<?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="screen_migration_message">"Гэта аднаразовы працэс, дзякуем за чаканне."</string>
<string name="screen_migration_title">"Налада ўліковага запісу."</string>
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string> <string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string> <string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string> <string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>

View file

@ -1,7 +1,5 @@
<?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="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string>
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string> <string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string> <string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string> <string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>

View file

@ -1,7 +1,5 @@
<?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="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Dein Konto wird eingerichtet."</string>
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string> <string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string> <string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string> <string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>

View file

@ -1,7 +1,5 @@
<?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="screen_migration_message">"Este proceso solo se hace una vez, gracias por esperar."</string>
<string name="screen_migration_title">"Configura tu cuenta"</string>
<string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string> <string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string>
<string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string> <string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string>
<string name="screen_welcome_bullet_1">"Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."</string> <string name="screen_welcome_bullet_1">"Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Il sagit dune opération ponctuelle, merci dattendre quelques instants."</string> <string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
<string name="screen_migration_title">"Configuration de votre compte."</string>
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string> <string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string> <string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>
<string name="screen_welcome_bullet_2">"Lhistorique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string> <string name="screen_welcome_bullet_2">"Lhistorique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Ez egy egyszeri folyamat, köszönjük a türelmét."</string> <string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
<string name="screen_migration_title">"A fiók beállítása."</string>
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string> <string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string> <string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string> <string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Ini adalah proses satu kali, terima kasih telah menunggu."</string> <string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
<string name="screen_migration_title">"Menyiapkan akun Anda."</string>
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string> <string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string> <string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string> <string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Si tratta di una procedura che si effettua una sola volta, grazie per l\'attesa."</string> <string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_migration_title">"Configurazione del tuo account."</string>
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string> <string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string> <string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string> <string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare."</string> <string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
<string name="screen_migration_title">"Contul dumneavoastră se configurează"</string>
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string> <string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string> <string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>
<string name="screen_welcome_button">"Să începem!"</string> <string name="screen_welcome_button">"Să începem!"</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string> <string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
<string name="screen_migration_title">"Настройка учетной записи."</string>
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string> <string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string> <string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string> <string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string> <string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string> <string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string> <string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string> <string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>

View file

@ -1,8 +1,6 @@
<?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="screen_migration_message">"這是一次性的程序,感謝您耐心等候。"</string> <string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string>
<string name="screen_migration_title">"正在設定您的帳號。"</string>
<string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string>
<string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string> <string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string>
<string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string> <string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string>
<string name="screen_welcome_button">"開始吧!"</string> <string name="screen_welcome_button">"開始吧!"</string>

View file

@ -1,7 +1,5 @@
<?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="screen_migration_message">"This is a one time process, thanks for waiting."</string>
<string name="screen_migration_title">"Setting up your account."</string>
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string> <string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string> <string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string> <string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>

View file

@ -18,16 +18,11 @@ package io.element.android.features.ftue.impl
import android.os.Build import android.os.Build
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.DefaultFtueState
import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
@ -54,7 +49,6 @@ class DefaultFtueStateTests {
fun `given all checks being true, should display flow is false`() = runTest { fun `given all checks being true, should display flow is false`() = runTest {
val welcomeState = FakeWelcomeState() val welcomeState = FakeWelcomeState()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
@ -63,14 +57,12 @@ class DefaultFtueStateTests {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
welcomeState = welcomeState, welcomeState = welcomeState,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
welcomeState.setWelcomeScreenShown() welcomeState.setWelcomeScreenShown()
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
state.updateState() state.updateState()
@ -85,7 +77,6 @@ class DefaultFtueStateTests {
fun `traverse flow`() = runTest { fun `traverse flow`() = runTest {
val welcomeState = FakeWelcomeState() val welcomeState = FakeWelcomeState()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
@ -94,29 +85,24 @@ class DefaultFtueStateTests {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
welcomeState = welcomeState, welcomeState = welcomeState,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
val steps = mutableListOf<FtueStep?>() val steps = mutableListOf<FtueStep?>()
// First step, migration screen // First step, welcome screen
steps.add(state.getNextStep(steps.lastOrNull()))
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
// Second step, welcome screen
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
welcomeState.setWelcomeScreenShown() welcomeState.setWelcomeScreenShown()
// Third step, notifications opt in // Second step, notifications opt in
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
// Fourth step, entering PIN code // Third step, entering PIN code
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
// Fifth step, analytics opt in // Fourth step, analytics opt in
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
@ -124,7 +110,6 @@ class DefaultFtueStateTests {
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
assertThat(steps).containsExactly( assertThat(steps).containsExactly(
FtueStep.MigrationScreen,
FtueStep.WelcomeScreen, FtueStep.WelcomeScreen,
FtueStep.NotificationsOptIn, FtueStep.NotificationsOptIn,
FtueStep.LockscreenSetup, FtueStep.LockscreenSetup,
@ -141,19 +126,16 @@ class DefaultFtueStateTests {
fun `if a check for a step is true, start from the next one`() = runTest { fun `if a check for a step is true, start from the next one`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val state = createState( val state = createState(
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
// Skip first 4 steps // Skip first 3 steps
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
state.setWelcomeScreenShown() state.setWelcomeScreenShown()
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
@ -171,18 +153,15 @@ class DefaultFtueStateTests {
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest { fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val state = createState( val state = createState(
sdkIntVersion = Build.VERSION_CODES.M, sdkIntVersion = Build.VERSION_CODES.M,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen) assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen)
state.setWelcomeScreenShown() state.setWelcomeScreenShown()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
@ -200,9 +179,7 @@ class DefaultFtueStateTests {
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
welcomeState: FakeWelcomeState = FakeWelcomeState(), welcomeState: FakeWelcomeState = FakeWelcomeState(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
matrixClient: MatrixClient = FakeMatrixClient(),
lockScreenService: LockScreenService = FakeLockScreenService(), lockScreenService: LockScreenService = FakeLockScreenService(),
// First version where notification permission is required // First version where notification permission is required
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
@ -211,9 +188,7 @@ class DefaultFtueStateTests {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
welcomeScreenState = welcomeState, welcomeScreenState = welcomeState,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
matrixClient = matrixClient,
) )
} }

View file

@ -25,11 +25,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle 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.R
import io.element.android.features.login.impl.error.isWaitListError 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
@ -123,7 +123,7 @@ private fun OverallContent(
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
if (state.loginAction !is AsyncData.Success) { if (state.loginAction !is AsyncData.Success) {
CompositionLocalProvider(LocalContentColor provides Color.Black) { CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textOnSolidPrimary) {
TextButton( TextButton(
text = stringResource(CommonStrings.action_cancel), text = stringResource(CommonStrings.action_cancel),
onClick = onCancelClicked, onClick = onCancelClicked,

View file

@ -55,6 +55,7 @@ dependencies {
implementation(projects.features.analytics.api) implementation(projects.features.analytics.api)
implementation(projects.features.ftue.api) implementation(projects.features.ftue.api)
implementation(projects.features.logout.api) implementation(projects.features.logout.api)
implementation(projects.features.roomlist.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api) implementation(projects.services.toolbox.api)
implementation(libs.datetime) implementation(libs.datetime)

View file

@ -24,6 +24,7 @@ import coil.annotation.ExperimentalCoilApi
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -45,6 +46,7 @@ class DefaultClearCacheUseCase @Inject constructor(
private val defaultCacheIndexProvider: DefaultCacheService, private val defaultCacheIndexProvider: DefaultCacheService,
private val okHttpClient: Provider<OkHttpClient>, private val okHttpClient: Provider<OkHttpClient>,
private val ftueState: FtueState, private val ftueState: FtueState,
private val migrationScreenStore: MigrationScreenStore,
) : ClearCacheUseCase { ) : ClearCacheUseCase {
override suspend fun invoke() = withContext(coroutineDispatchers.io) { override suspend fun invoke() = withContext(coroutineDispatchers.io) {
// Clear Matrix cache // Clear Matrix cache
@ -60,6 +62,8 @@ class DefaultClearCacheUseCase @Inject constructor(
context.cacheDir.deleteRecursively() context.cacheDir.deleteRecursively()
// Clear some settings // Clear some settings
ftueState.reset() ftueState.reset()
// Clear migration screen store
migrationScreenStore.reset()
// Ensure the app is restarted // Ensure the app is restarted
defaultCacheIndexProvider.onClearedCache(matrixClient.sessionId) defaultCacheIndexProvider.onClearedCache(matrixClient.sessionId)
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.api.migration
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId

View file

@ -33,6 +33,7 @@ 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.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -63,6 +64,7 @@ class RoomListPresenter @Inject constructor(
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
private val migrationScreenPresenter: MigrationScreenPresenter,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
@Composable @Composable
override fun present(): RoomListState { override fun present(): RoomListState {
@ -82,6 +84,8 @@ class RoomListPresenter @Inject constructor(
initialLoad(matrixUser) initialLoad(matrixUser)
} }
val isMigrating = migrationScreenPresenter.present().isMigrating
// Session verification status (unknown, not verified, verified) // Session verification status (unknown, not verified, verified)
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) } var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
@ -148,6 +152,7 @@ class RoomListPresenter @Inject constructor(
displaySearchResults = displaySearchResults, displaySearchResults = displaySearchResults,
contextMenu = contextMenu, contextMenu = contextMenu,
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,
displayMigrationStatus = isMigrating,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -40,6 +40,7 @@ data class RoomListState(
val displaySearchResults: Boolean, val displaySearchResults: Boolean,
val contextMenu: ContextMenu, val contextMenu: ContextMenu,
val leaveRoomState: LeaveRoomState, val leaveRoomState: LeaveRoomState,
val displayMigrationStatus: Boolean,
val eventSink: (RoomListEvents) -> Unit, val eventSink: (RoomListEvents) -> Unit,
) { ) {
sealed interface ContextMenu { sealed interface ContextMenu {

View file

@ -53,6 +53,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState().copy(displayRecoveryKeyPrompt = true), aRoomListState().copy(displayRecoveryKeyPrompt = true),
aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())),
aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())),
aRoomListState().copy(matrixUser = null, displayMigrationStatus = true),
) )
} }
@ -70,6 +71,7 @@ internal fun aRoomListState() = RoomListState(
displaySearchResults = false, displaySearchResults = false,
contextMenu = RoomListState.ContextMenu.Hidden, contextMenu = RoomListState.ContextMenu.Hidden,
leaveRoomState = aLeaveRoomState(), leaveRoomState = aLeaveRoomState(),
displayMigrationStatus = false,
eventSink = {} eventSink = {}
) )

View file

@ -54,6 +54,7 @@ import io.element.android.features.roomlist.impl.components.RequestVerificationH
import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.migration.MigrationScreenView
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.RoomListSearchResultView import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -212,6 +213,7 @@ private fun RoomListContent(
onMenuActionClicked = onMenuActionClicked, onMenuActionClicked = onMenuActionClicked,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
displayMenuItems = !state.displayMigrationStatus,
) )
}, },
content = { padding -> content = { padding ->
@ -270,18 +272,22 @@ private fun RoomListContent(
} }
} }
} }
MigrationScreenView(isMigrating = state.displayMigrationStatus)
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton( if (!state.displayMigrationStatus) {
// FIXME align on Design system theme FloatingActionButton(
containerColor = MaterialTheme.colorScheme.primary, // FIXME align on Design system theme
onClick = onCreateRoomClicked containerColor = MaterialTheme.colorScheme.primary,
) { onClick = onCreateRoomClicked
Icon( ) {
// Note cannot use Icons.Outlined.EditSquare, it does not exist :/ Icon(
resourceId = CommonDrawables.ic_new_message, // Note cannot use Icons.Outlined.EditSquare, it does not exist :/
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message) resourceId = CommonDrawables.ic_new_message,
) contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message)
)
}
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },

View file

@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@ -68,6 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@ -89,6 +92,7 @@ fun RoomListTopBar(
onMenuActionClicked: (RoomListMenuAction) -> Unit, onMenuActionClicked: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
displayMenuItems: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun closeFilter() { fun closeFilter() {
@ -108,6 +112,7 @@ fun RoomListTopBar(
onSearchClicked = onToggleSearch, onSearchClicked = onToggleSearch,
onMenuActionClicked = onMenuActionClicked, onMenuActionClicked = onMenuActionClicked,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
displayMenuItems = displayMenuItems,
modifier = modifier, modifier = modifier,
) )
} }
@ -122,6 +127,7 @@ private fun DefaultRoomListTopBar(
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onSearchClicked: () -> Unit, onSearchClicked: () -> Unit,
onMenuActionClicked: (RoomListMenuAction) -> Unit, onMenuActionClicked: (RoomListMenuAction) -> Unit,
displayMenuItems: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
// We need this to manually clip the top app bar in preview mode // We need this to manually clip the top app bar in preview mode
@ -195,79 +201,89 @@ private fun DefaultRoomListTopBar(
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title)) Text(text = stringResource(id = R.string.screen_roomlist_main_space_title))
}, },
navigationIcon = { navigationIcon = {
avatarData?.let { IconButton(
IconButton( modifier = Modifier.testTag(TestTags.homeScreenSettings),
modifier = Modifier.testTag(TestTags.homeScreenSettings), onClick = onOpenSettings
onClick = onOpenSettings ) {
) { if (avatarData != null) {
Avatar( Avatar(
avatarData = it, avatarData = avatarData!!,
contentDescription = stringResource(CommonStrings.common_settings), contentDescription = stringResource(CommonStrings.common_settings),
) )
if (showAvatarIndicator) { } else {
RedIndicatorAtom( // Placeholder avatar until the avatarData is available
modifier = Modifier Surface(
.padding(4.5.dp) modifier = Modifier.size(AvatarSize.CurrentUserTopBar.dp),
.align(Alignment.TopEnd) shape = CircleShape,
) color = ElementTheme.colors.iconSecondary,
} content = {}
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier
.padding(4.5.dp)
.align(Alignment.TopEnd)
)
} }
} }
}, },
actions = { actions = {
IconButton( if (displayMenuItems) {
onClick = onSearchClicked,
) {
Icon(
imageVector = CompoundIcons.Search,
contentDescription = stringResource(CommonStrings.action_search),
)
}
if (RoomListConfig.HAS_DROP_DOWN_MENU) {
var showMenu by remember { mutableStateOf(false) }
IconButton( IconButton(
onClick = { showMenu = !showMenu } onClick = onSearchClicked,
) { ) {
Icon( Icon(
imageVector = CompoundIcons.OverflowVertical, imageVector = CompoundIcons.Search,
contentDescription = null, contentDescription = stringResource(CommonStrings.action_search),
) )
} }
DropdownMenu( if (RoomListConfig.HAS_DROP_DOWN_MENU) {
expanded = showMenu, var showMenu by remember { mutableStateOf(false) }
onDismissRequest = { showMenu = false } IconButton(
) { onClick = { showMenu = !showMenu }
if (RoomListConfig.SHOW_INVITE_MENU_ITEM) { ) {
DropdownMenuItem( Icon(
onClick = { imageVector = CompoundIcons.OverflowVertical,
showMenu = false contentDescription = null,
onMenuActionClicked(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ShareAndroid,
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
) )
} }
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) { DropdownMenu(
DropdownMenuItem( expanded = showMenu,
onClick = { onDismissRequest = { showMenu = false }
showMenu = false ) {
onMenuActionClicked(RoomListMenuAction.ReportBug) if (RoomListConfig.SHOW_INVITE_MENU_ITEM) {
}, DropdownMenuItem(
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) }, onClick = {
leadingIcon = { showMenu = false
Icon( onMenuActionClicked(RoomListMenuAction.InviteFriends)
imageVector = CompoundIcons.ChatProblem, },
tint = ElementTheme.materialColors.secondary, text = { Text(stringResource(id = CommonStrings.action_invite)) },
contentDescription = null, leadingIcon = {
) Icon(
} imageVector = CompoundIcons.ShareAndroid,
) tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
)
}
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
leadingIcon = {
Icon(
imageVector = CompoundIcons.ChatProblem,
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
)
}
} }
} }
} }
@ -299,6 +315,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {}, onOpenSettings = {},
onSearchClicked = {}, onSearchClicked = {},
displayMenuItems = true,
onMenuActionClicked = {}, onMenuActionClicked = {},
) )
} }
@ -314,6 +331,7 @@ internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {}, onOpenSettings = {},
onSearchClicked = {}, onSearchClicked = {},
displayMenuItems = true,
onMenuActionClicked = {}, onMenuActionClicked = {},
) )
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.impl.migration
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
@ -32,13 +36,15 @@ class MigrationScreenPresenter @Inject constructor(
@Composable @Composable
override fun present(): MigrationScreenState { override fun present(): MigrationScreenState {
val roomListState by matrixClient.roomListService.state.collectAsState() val roomListState by matrixClient.roomListService.state.collectAsState()
var needsMigration by remember { mutableStateOf(migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)) }
if (roomListState == RoomListService.State.Running) { if (roomListState == RoomListService.State.Running) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
needsMigration = false
migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId) migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId)
} }
} }
return MigrationScreenState( return MigrationScreenState(
isMigrating = roomListState != RoomListService.State.Running isMigrating = needsMigration && roomListState != RoomListService.State.Running
) )
} }
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.impl.migration
data class MigrationScreenState( data class MigrationScreenState(
val isMigrating: Boolean val isMigrating: Boolean

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,45 +14,43 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.impl.migration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.ftue.impl.R import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@Composable @Composable
fun MigrationScreenView( fun MigrationScreenView(
migrationState: MigrationScreenState, isMigrating: Boolean,
onMigrationFinished: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (migrationState.isMigrating.not()) { val displayMigrationStatusFadeProgress by animateFloatAsState(
val latestOnMigrationFinished by rememberUpdatedState(onMigrationFinished) targetValue = if (isMigrating) 1f else 0f,
LaunchedEffect(Unit) { animationSpec = tween(durationMillis = 200),
latestOnMigrationFinished() label = "Migration view fade"
}
}
SunsetPage(
modifier = modifier,
isLoading = true,
title = stringResource(id = R.string.screen_migration_title),
subtitle = stringResource(id = R.string.screen_migration_message),
overallContent = {}
) )
if (displayMigrationStatusFadeProgress > 0f) {
SunsetPage(
modifier = modifier.alpha(displayMigrationStatusFadeProgress),
isLoading = true,
title = stringResource(id = R.string.screen_migration_title),
subtitle = stringResource(id = R.string.screen_migration_message),
overallContent = {}
)
}
} }
@PreviewsDayNight
@Composable @Composable
@PreviewsDayNight
internal fun MigrationViewPreview() = ElementPreview { internal fun MigrationViewPreview() = ElementPreview {
MigrationScreenView( MigrationScreenView(isMigrating = true)
migrationState = MigrationScreenState(isMigrating = true),
onMigrationFinished = {}
)
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,11 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.impl.migration
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.DefaultPreferences

View file

@ -2,6 +2,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="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to confirm 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 confirm your recovery key to maintain access to your chat backup."</string>
<string name="confirm_recovery_key_banner_title">"Confirm your recovery key"</string> <string name="confirm_recovery_key_banner_title">"Confirm your recovery key"</string>
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
<string name="screen_migration_title">"Setting up your account."</string>
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string> <string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
<string name="screen_roomlist_empty_message">"Get started by messaging someone."</string> <string name="screen_roomlist_empty_message">"Get started by messaging someone."</string>
<string name="screen_roomlist_empty_title">"No chats yet."</string> <string name="screen_roomlist_empty_title">"No chats yet."</string>

View file

@ -29,6 +29,8 @@ import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
@ -44,12 +46,14 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
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_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -396,6 +400,36 @@ class RoomListPresenterTests {
} }
} }
@Test
fun `present - change in migration presenter state modifies isMigrating`() = runTest {
val client = FakeMatrixClient(sessionId = A_SESSION_ID)
val migrationStore = InMemoryMigrationScreenStore()
val migrationScreenPresenter = MigrationScreenPresenter(client, migrationStore)
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(
client = client,
coroutineScope = scope,
migrationScreenPresenter = migrationScreenPresenter,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// The migration screen is shown if the migration screen has not been shown before
assertThat(initialState.displayMigrationStatus).isTrue()
skipItems(2)
// Set migration as done and set the room list service as running to trigger a refresh of the presenter value
(client.roomListService as FakeRoomListService).postState(RoomListService.State.Running)
migrationStore.setMigrationScreenShown(A_SESSION_ID)
// The migration screen is not shown anymore
assertThat(awaitItem().displayMigrationStatus).isFalse()
cancelAndIgnoreRemainingEvents()
scope.cancel()
}
}
private fun TestScope.createRoomListPresenter( private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(),
@ -409,6 +443,10 @@ class RoomListPresenterTests {
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter(
matrixClient = client,
migrationScreenStore = InMemoryMigrationScreenStore(),
)
) = RoomListPresenter( ) = RoomListPresenter(
client = client, client = client,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
@ -433,6 +471,7 @@ class RoomListPresenterTests {
encryptionService = encryptionService, encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
), ),
migrationScreenPresenter = migrationScreenPresenter,
) )
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,8 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.impl.migration
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
class InMemoryMigrationScreenStore : MigrationScreenStore { class InMemoryMigrationScreenStore : MigrationScreenStore {

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.ftue.impl.migration package io.element.android.features.roomlist.impl.migration
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
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.api.migration.MigrationScreenStore
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -61,6 +62,7 @@ class MigrationScreenPresenterTest {
val nextState = awaitItem() val nextState = awaitItem()
assertThat(nextState.isMigrating).isFalse() assertThat(nextState.isMigrating).isFalse()
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse() assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse()
cancelAndIgnoreRemainingEvents()
} }
} }

View file

@ -37,8 +37,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.annotations.CoreColorToken
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.compound.tokens.generated.internal.DarkColorTokens
import io.element.android.compound.tokens.generated.internal.LightColorTokens
import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.R
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -54,7 +56,11 @@ fun SunsetPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
overallContent: @Composable () -> Unit, overallContent: @Composable () -> Unit,
) { ) {
ForcedDarkElementTheme(lightStatusBar = true) { ElementTheme(
// Always use the opposite value of the current theme
darkTheme = ElementTheme.isLightTheme,
applySystemBarsUpdate = false,
) {
Box( Box(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
@ -107,21 +113,34 @@ fun SunsetPage(
} }
} }
@OptIn(CoreColorToken::class)
@Composable @Composable
private fun SunsetBackground( private fun SunsetBackground(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier.fillMaxSize()) { Column(modifier = modifier.fillMaxSize()) {
// The top background colors are the opposite of the current theme ones
val topBackgroundColor = if (ElementTheme.isLightTheme) {
DarkColorTokens.colorThemeBg
} else {
LightColorTokens.colorThemeBg
}
// The bottom background colors follow the current theme
val bottomBackgroundColor = if (ElementTheme.isLightTheme) {
LightColorTokens.colorThemeBg
} else {
// The dark background color doesn't 100% match the image, so we use a custom color
Color(0xFF121418)
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(0.3f) .weight(0.3f)
.background(Color.White) .background(topBackgroundColor)
) )
Image( Image(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(), painter = painterResource(id = R.drawable.bg_migration),
painter = painterResource(id = R.drawable.light_dark),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
contentDescription = null, contentDescription = null,
) )
@ -129,7 +148,7 @@ private fun SunsetBackground(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(0.7f) .weight(0.7f)
.background(Color(0xFF121418)) .background(bottomBackgroundColor)
) )
} }
} }

View file

@ -23,7 +23,6 @@ import android.os.Build
import android.text.TextPaint import android.text.TextPaint
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
@ -136,13 +135,13 @@ object BloomDefaults {
@Composable @Composable
fun defaultLayers() = persistentListOf( fun defaultLayers() = persistentListOf(
// Bottom layer // Bottom layer
if (isSystemInDarkTheme()) { if (ElementTheme.isLightTheme) {
BloomLayer(0.5f, BlendMode.Exclusion)
} else {
BloomLayer(0.2f, BlendMode.Hardlight) BloomLayer(0.2f, BlendMode.Hardlight)
} else {
BloomLayer(0.5f, BlendMode.Exclusion)
}, },
// Top layer // Top layer
BloomLayer(if (isSystemInDarkTheme()) 0.2f else 0.8f, BlendMode.Color), BloomLayer(if (ElementTheme.isLightTheme) 0.8f else 0.2f, BlendMode.Color),
) )
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

View file

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Before After
Before After

View file

@ -28,6 +28,8 @@ import io.element.android.features.roomlist.impl.RoomListView
import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDataSource import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.dateformatter.impl.DateFormatters import io.element.android.libraries.dateformatter.impl.DateFormatters
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
@ -103,6 +105,10 @@ class RoomListScreen(
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
), ),
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
migrationScreenPresenter = MigrationScreenPresenter(
matrixClient = matrixClient,
migrationScreenStore = SharedPrefsMigrationScreenStore(context.getSharedPreferences("migration", Context.MODE_PRIVATE))
)
) )
@Composable @Composable

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ce7cb3f4a50912cd7aa96340ff850d438a26e5efc331173c658448fdbecc7b65 oid sha256:9db077d2e2981e9b3d431312b171741505e22816fa4b2bb3699f6ae4e00b48e9
size 145047 size 145160

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b06c44fcc0afee1a398dbddeb18253f25002c3c8e77929d049edd878b3927e8a oid sha256:de09324bd0a41e25eff91ae77d5d88d96289a379ecf24e626fa9ea09a5acd43b
size 145776 size 145928

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:30a12c0e23d30c5841fef2f705af3fc4ea691ff06425d84a5c33f34809b78d8c oid sha256:54f47ca8a687adf5528e5ff8ddd87d901d2aa1280e6252ec962c176d6a207237
size 65492 size 65584

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ce7cb3f4a50912cd7aa96340ff850d438a26e5efc331173c658448fdbecc7b65 oid sha256:9db077d2e2981e9b3d431312b171741505e22816fa4b2bb3699f6ae4e00b48e9
size 145047 size 145160

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ce7cb3f4a50912cd7aa96340ff850d438a26e5efc331173c658448fdbecc7b65 oid sha256:5d5c0f2cf39d75b09a8cc1f4ffbd65912cab79ce5881ed5da5fdadeb9f57cb00
size 145047 size 170070

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b06c44fcc0afee1a398dbddeb18253f25002c3c8e77929d049edd878b3927e8a oid sha256:c23b11ebcc1e73255714b07cee27c4583a9923112cdf0eda60983925d8074bb7
size 145776 size 170640

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a6687f1be6e8abab9ae846152cc4edeb5fe1becda4ec9bcf64c255ded9dc937a oid sha256:026200b29fb2599e1f04ae19ab75cfed41a8c4aff5b0ef0086a6948244733830
size 63857 size 67437

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ce7cb3f4a50912cd7aa96340ff850d438a26e5efc331173c658448fdbecc7b65 oid sha256:5d5c0f2cf39d75b09a8cc1f4ffbd65912cab79ce5881ed5da5fdadeb9f57cb00
size 145047 size 170070

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ddf3e62bd00f179fbeed4e432bd9a7a21321e83dd1d5192e3dc213fcfbc8c372 oid sha256:97aa5dbc8ddcd05a44ae56771a13085096f11b2469c1a7edb3281e242d86ad21
size 128199 size 150264

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:50ca2afda159bdd7f57e78b04fbe3c022a9aeee1b68f72cf4d4269b3e503f0e5 oid sha256:d56b02025c9de6cca3aaa3e4cc5cd82b222790c4efa0013f957307c13e45ee40
size 127335 size 151229

View file

@ -106,7 +106,8 @@
"includeRegex" : [ "includeRegex" : [
"screen_roomlist_.*", "screen_roomlist_.*",
"session_verification_banner_.*", "session_verification_banner_.*",
"confirm_recovery_key_banner_.*" "confirm_recovery_key_banner_.*",
"screen_migration_.*"
] ]
}, },
{ {
@ -154,7 +155,6 @@
"name" : ":features:ftue:impl", "name" : ":features:ftue:impl",
"includeRegex" : [ "includeRegex" : [
"screen_welcome_.*", "screen_welcome_.*",
"screen_migration_.*",
"screen_notification_optin_.*" "screen_notification_optin_.*"
] ]
}, },