From 9de16fc74f05ea4c83a61031b82cb84d978e24bd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Oct 2025 22:04:31 +0200 Subject: [PATCH 01/17] Format --- .../src/main/kotlin/io/element/android/appnav/RootFlowNode.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 93df005c76..7f5cb13246 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -64,7 +64,9 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber -@ContributesNode(AppScope::class) @AssistedInject class RootFlowNode( +@ContributesNode(AppScope::class) +@AssistedInject +class RootFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, private val sessionStore: SessionStore, From 23ed5e71fdfeb7c02e5dd565aa22abd0e8437bce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Oct 2025 22:05:38 +0200 Subject: [PATCH 02/17] Add Composable for a Beta label --- .../designsystem/atomic/atoms/BetaLabel.kt | 66 +++++++++++++++++++ .../molecules/IconTitleSubtitleMolecule.kt | 35 ++++++---- 2 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt new file mode 100644 index 0000000000..7ef5b5dec5 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.atomic.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.tokens.generated.internal.DarkColorTokens +import io.element.android.compound.tokens.generated.internal.LightColorTokens +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text + +@OptIn(CoreColorToken::class) +@Composable +fun BetaLabel( + modifier: Modifier = Modifier, +) { + val (backgroundColor, borderColor, textColor) = if (ElementTheme.isLightTheme) { + listOf( + LightColorTokens.colorGreen300, + LightColorTokens.colorGreen700, + LightColorTokens.colorGreen900, + ) + } else { + listOf( + DarkColorTokens.colorGreen300, + DarkColorTokens.colorGreen700, + DarkColorTokens.colorGreen900, + ) + } + val shape = RoundedCornerShape(size = 6.dp) + Text( + modifier = modifier + .border( + width = 1.dp, + color = borderColor, + shape = shape, + ) + .background( + color = backgroundColor, + shape = shape, + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + text = "BETA", + style = ElementTheme.typography.fontBodySmMedium, + color = textColor, + ) +} + +@PreviewsDayNight +@Composable +internal fun BetaLabelPreview() = ElementPreview { + BetaLabel() +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt index a89a21adbd..be149437f1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt @@ -7,7 +7,9 @@ package io.element.android.libraries.designsystem.atomic.molecules +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -20,6 +22,7 @@ import androidx.compose.ui.text.style.TextAlign 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.libraries.designsystem.atomic.atoms.BetaLabel import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -32,6 +35,7 @@ import io.element.android.libraries.designsystem.theme.components.Text * @param subTitle the subtitle to display * @param iconStyle the style of the [BigIcon] to display * @param modifier the modifier to apply to this layout + * @param showBetaLabel whether to show a "BETA" label next to the title */ @Composable fun IconTitleSubtitleMolecule( @@ -39,6 +43,7 @@ fun IconTitleSubtitleMolecule( subTitle: String?, iconStyle: BigIcon.Style, modifier: Modifier = Modifier, + showBetaLabel: Boolean = false, ) { Column(modifier) { BigIcon( @@ -46,17 +51,25 @@ fun IconTitleSubtitleMolecule( style = iconStyle, ) Spacer(modifier = Modifier.height(16.dp)) - Text( - text = title, - modifier = Modifier - .fillMaxWidth() - .semantics { - heading() - }, - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontHeadingMdBold, - color = ElementTheme.colors.textPrimary, - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally) + ) { + Text( + text = title, + modifier = Modifier + .semantics { + heading() + }, + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontHeadingMdBold, + color = ElementTheme.colors.textPrimary, + ) + if (showBetaLabel) { + BetaLabel() + } + } if (subTitle != null) { Spacer(Modifier.height(8.dp)) Text( From 9f71bc4575e0451f2aabb803afd041bc3ca25475 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Oct 2025 22:06:21 +0200 Subject: [PATCH 03/17] Add a way to use the primary color for the icon. --- .../android/libraries/designsystem/components/BigIcon.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt index f469555ee4..78a4d32e44 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt @@ -53,11 +53,13 @@ object BigIcon { * @param vectorIcon the [ImageVector] to display * @param contentDescription the content description of the icon, if any. It defaults to `null` * @param useCriticalTint whether the icon and background should be rendered using critical tint + * @param usePrimaryTint whether the icon should be rendered using primary tint */ data class Default( val vectorIcon: ImageVector, val contentDescription: String? = null, val useCriticalTint: Boolean = false, + val usePrimaryTint: Boolean = false, ) : Style /** @@ -143,6 +145,8 @@ object BigIcon { val iconTint = when (style) { is Style.Default -> if (style.useCriticalTint) { ElementTheme.colors.iconCriticalPrimary + } else if (style.usePrimaryTint) { + ElementTheme.colors.iconPrimary } else { ElementTheme.colors.iconSecondary } From 2907f762f62a6128fc5d6a287504b4311afb1b74 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Oct 2025 23:13:43 +0200 Subject: [PATCH 04/17] Announcement for Spaces --- appnav/build.gradle.kts | 1 + .../io/element/android/appnav/RootFlowNode.kt | 5 + .../android/appnav/root/RootPresenter.kt | 4 + .../element/android/appnav/root/RootState.kt | 2 + .../android/appnav/root/RootStateProvider.kt | 2 + .../element/android/appnav/root/RootView.kt | 8 + .../android/appnav/RootPresenterTest.kt | 2 + features/announcement/api/build.gradle.kts | 13 ++ .../announcement/api/AnnouncementService.kt | 21 +++ .../announcement/api/AnnouncementState.kt | 18 ++ features/announcement/impl/build.gradle.kts | 36 ++++ .../impl/AnnouncementPresenter.kt | 35 ++++ .../impl/DefaultAnnouncementService.kt | 55 ++++++ .../impl/di/AnnouncementModule.kt | 23 +++ .../impl/spaces/SpaceAnnouncementEvents.kt | 12 ++ .../impl/spaces/SpaceAnnouncementNode.kt | 35 ++++ .../impl/spaces/SpaceAnnouncementPresenter.kt | 42 +++++ .../impl/spaces/SpaceAnnouncementState.kt | 14 ++ .../spaces/SpaceAnnouncementStateProvider.kt | 27 +++ .../impl/spaces/SpaceAnnouncementView.kt | 165 ++++++++++++++++++ .../impl/store/AnnouncementStore.kt | 23 +++ .../impl/store/DefaultAnnouncementStore.kt | 44 +++++ .../impl/src/main/res/values/localazy.xml | 11 ++ .../impl/AnnouncementPresenterTest.kt | 18 ++ features/announcement/test/build.gradle.kts | 19 ++ .../test/logs/FakeAnnouncementService.kt | 28 +++ features/home/impl/build.gradle.kts | 2 + .../features/home/impl/HomePresenter.kt | 7 +- .../features/home/impl/HomePresenterTest.kt | 13 ++ tools/localazy/config.json | 6 + 30 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 features/announcement/api/build.gradle.kts create mode 100644 features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt create mode 100644 features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt create mode 100644 features/announcement/impl/build.gradle.kts create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt create mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt create mode 100644 features/announcement/impl/src/main/res/values/localazy.xml create mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt create mode 100644 features/announcement/test/build.gradle.kts create mode 100644 features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 7f487a1e63..063e26b9da 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(libs.coil) + implementation(projects.features.announcement.api) implementation(projects.features.ftue.api) implementation(projects.features.share.api) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 7f5cb13246..c01b42af37 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -34,6 +34,7 @@ import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView +import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint @@ -81,6 +82,7 @@ class RootFlowNode( private val oidcActionFlow: OidcActionFlow, private val bugReporter: BugReporter, private val featureFlagService: FeatureFlagService, + private val announcementService: AnnouncementService, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.SplashScreen, @@ -172,6 +174,9 @@ class RootFlowNode( state = state, modifier = modifier, onOpenBugReport = this::onOpenBugReport, + announcementRenderer = { state, announcementModifier -> + announcementService.Render(state, announcementModifier) + } ) { val backstackSlider = rememberBackstackSlider( transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt index d987c2a7ec..928e551a56 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.SuperProperties +import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.libraries.architecture.Presenter @@ -24,6 +25,7 @@ import io.element.android.services.apperror.api.AppErrorStateService class RootPresenter( private val crashDetectionPresenter: Presenter, private val rageshakeDetectionPresenter: Presenter, + private val announcementPresenter: Presenter, private val appErrorStateService: AppErrorStateService, private val analyticsService: AnalyticsService, private val sdkMetadata: SdkMetadata, @@ -32,6 +34,7 @@ class RootPresenter( override fun present(): RootState { val rageshakeDetectionState = rageshakeDetectionPresenter.present() val crashDetectionState = crashDetectionPresenter.present() + val announcementState = announcementPresenter.present() val appErrorState by appErrorStateService.appErrorStateFlow.collectAsState() LaunchedEffect(Unit) { @@ -48,6 +51,7 @@ class RootPresenter( rageshakeDetectionState = rageshakeDetectionState, crashDetectionState = crashDetectionState, errorState = appErrorState, + announcementState = announcementState, ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt index 3ea7362efa..5ab995246f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt @@ -8,6 +8,7 @@ package io.element.android.appnav.root import androidx.compose.runtime.Immutable +import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.services.apperror.api.AppErrorState @@ -17,4 +18,5 @@ data class RootState( val rageshakeDetectionState: RageshakeDetectionState, val crashDetectionState: CrashDetectionState, val errorState: AppErrorState, + val announcementState: AnnouncementState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt index 4d84e06070..896e62d820 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.appnav.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.announcement.api.anAnnouncementState import io.element.android.features.rageshake.api.crash.aCrashDetectionState import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState import io.element.android.services.apperror.api.AppErrorState @@ -33,5 +34,6 @@ open class RootStateProvider : PreviewParameterProvider { fun aRootState() = RootState( rageshakeDetectionState = aRageshakeDetectionState(), crashDetectionState = aCrashDetectionState(), + announcementState = anAnnouncementState(), errorState = AppErrorState.NoError, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index bd7db5e9c2..275ccaae84 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.rageshake.api.crash.CrashDetectionEvents import io.element.android.features.rageshake.api.crash.CrashDetectionView import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents @@ -27,6 +28,7 @@ import io.element.android.services.apperror.impl.AppErrorView fun RootView( state: RootState, onOpenBugReport: () -> Unit, + announcementRenderer: @Composable (AnnouncementState, Modifier) -> Unit, modifier: Modifier = Modifier, children: @Composable BoxScope.() -> Unit, ) { @@ -43,6 +45,11 @@ fun RootView( onOpenBugReport.invoke() } + announcementRenderer( + state.announcementState, + Modifier, + ) + RageshakeDetectionView( state = state.rageshakeDetectionState, onOpenBugReport = ::onOpenBugReport, @@ -63,6 +70,7 @@ internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootSta RootView( state = rootState, onOpenBugReport = {}, + announcementRenderer = { _, _ -> }, ) { Text("Children") } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 2a343a1592..9d0d1e9572 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -12,6 +12,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.appnav.root.RootPresenter +import io.element.android.features.announcement.api.anAnnouncementState import io.element.android.features.rageshake.api.crash.aCrashDetectionState import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState import io.element.android.libraries.matrix.test.FakeSdkMetadata @@ -71,6 +72,7 @@ class RootPresenterTest { return RootPresenter( crashDetectionPresenter = { aCrashDetectionState() }, rageshakeDetectionPresenter = { aRageshakeDetectionState() }, + announcementPresenter = { anAnnouncementState() }, appErrorStateService = appErrorService, analyticsService = FakeAnalyticsService(), sdkMetadata = FakeSdkMetadata("sha") diff --git a/features/announcement/api/build.gradle.kts b/features/announcement/api/build.gradle.kts new file mode 100644 index 0000000000..0fa87f039e --- /dev/null +++ b/features/announcement/api/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.announcement.api" +} diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt new file mode 100644 index 0000000000..abd50aef3b --- /dev/null +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.api + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +interface AnnouncementService { + suspend fun onEnteringSpaceTab() + + @Composable + fun Render( + state: AnnouncementState, + modifier: Modifier, + ) +} diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt new file mode 100644 index 0000000000..eccdbd237c --- /dev/null +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.api + +data class AnnouncementState( + val showSpaceAnnouncement: Boolean, +) + +fun anAnnouncementState( + showSpaceAnnouncement: Boolean = false, +) = AnnouncementState( + showSpaceAnnouncement = showSpaceAnnouncement, +) diff --git a/features/announcement/impl/build.gradle.kts b/features/announcement/impl/build.gradle.kts new file mode 100644 index 0000000000..0ef6d09061 --- /dev/null +++ b/features/announcement/impl/build.gradle.kts @@ -0,0 +1,36 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.announcement.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +setupDependencyInjection() + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.preferences.api) + implementation(projects.libraries.uiStrings) + api(projects.features.announcement.api) + implementation(libs.androidx.datastore.preferences) + + testCommonDependencies(libs) +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt new file mode 100644 index 0000000000..435d69d1f5 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject +import io.element.android.features.announcement.api.AnnouncementState +import io.element.android.features.announcement.impl.store.AnnouncementStore +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.flow.map + +@Inject +class AnnouncementPresenter( + private val announcementStore: AnnouncementStore, +) : Presenter { + @Composable + override fun present(): AnnouncementState { + val showSpaceAnnouncement by remember { + announcementStore.spaceAnnouncementFlow().map { + it == AnnouncementStore.SpaceAnnouncement.Show + } + }.collectAsState(false) + return AnnouncementState( + showSpaceAnnouncement = showSpaceAnnouncement, + ) + } +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt new file mode 100644 index 0000000000..fb82246a3c --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.features.announcement.api.AnnouncementService +import io.element.android.features.announcement.api.AnnouncementState +import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter +import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView +import io.element.android.features.announcement.impl.store.AnnouncementStore +import kotlinx.coroutines.flow.first + +@ContributesBinding(AppScope::class) +@Inject +class DefaultAnnouncementService( + private val announcementStore: AnnouncementStore, + private val spaceAnnouncementPresenter: SpaceAnnouncementPresenter, +) : AnnouncementService { + override suspend fun onEnteringSpaceTab() { + val currentValue = announcementStore.spaceAnnouncementFlow().first() + if (currentValue == AnnouncementStore.SpaceAnnouncement.NeverShown) { + announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show) + } + } + + @Composable + override fun Render(state: AnnouncementState, modifier: Modifier) { + Box(modifier = modifier.fillMaxSize()) { + AnimatedVisibility( + visible = state.showSpaceAnnouncement, + enter = fadeIn(), + exit = fadeOut(), + ) { + val spaceAnnouncementState = spaceAnnouncementPresenter.present() + SpaceAnnouncementView( + state = spaceAnnouncementState, + ) + } + } + } +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt new file mode 100644 index 0000000000..64998dfe1a --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import io.element.android.features.announcement.api.AnnouncementState +import io.element.android.features.announcement.impl.AnnouncementPresenter +import io.element.android.libraries.architecture.Presenter + +@ContributesTo(AppScope::class) +@BindingContainer +interface AnnouncementModule { + @Binds + fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt new file mode 100644 index 0000000000..9741608b1e --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +sealed interface SpaceAnnouncementEvents { + data object Continue : SpaceAnnouncementEvents +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt new file mode 100644 index 0000000000..ba8f74d803 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +@AssistedInject +class SpaceAnnouncementNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SpaceAnnouncementPresenter, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + SpaceAnnouncementView( + state = state, + modifier = modifier, + ) + } +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt new file mode 100644 index 0000000000..dbe619a867 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import dev.zacsweers.metro.Inject +import io.element.android.features.announcement.impl.store.AnnouncementStore +import io.element.android.features.announcement.impl.store.AnnouncementStore.SpaceAnnouncement +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import kotlinx.coroutines.launch + +@Inject +class SpaceAnnouncementPresenter( + private val buildMeta: BuildMeta, + private val announcementStore: AnnouncementStore, +) : Presenter { + @Composable + override fun present(): SpaceAnnouncementState { + val localCoroutineScope = rememberCoroutineScope() + + fun handleEvents(event: SpaceAnnouncementEvents) { + when (event) { + SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch { + announcementStore.setSpaceAnnouncementValue(SpaceAnnouncement.Shown) + } + } + } + + return SpaceAnnouncementState( + applicationName = buildMeta.applicationName, + desktopApplicationName = buildMeta.desktopApplicationName, + eventSink = ::handleEvents + ) + } +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt new file mode 100644 index 0000000000..f02519a405 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +data class SpaceAnnouncementState( + val applicationName: String, + val desktopApplicationName: String, + val eventSink: (SpaceAnnouncementEvents) -> Unit +) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt new file mode 100644 index 0000000000..0eb2e8ff48 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class SpaceAnnouncementStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSpaceAnnouncementState(), + ) +} + +fun aSpaceAnnouncementState( + applicationName: String = "Element", + desktopApplicationName: String = "Element", + eventSink: (SpaceAnnouncementEvents) -> Unit = {}, +) = SpaceAnnouncementState( + applicationName = applicationName, + desktopApplicationName = desktopApplicationName, + eventSink = eventSink, +) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt new file mode 100644 index 0000000000..58799c05bd --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +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.announcement.impl.R +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +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.Text +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf + +/** + * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181 + */ +@Composable +fun SpaceAnnouncementView( + state: SpaceAnnouncementState, + modifier: Modifier = Modifier, +) { + val eventSink = state.eventSink + + fun onContinue() { + eventSink(SpaceAnnouncementEvents.Continue) + } + + BackHandler { + state.eventSink(SpaceAnnouncementEvents.Continue) + } + HeaderFooterPage( + modifier = modifier, + isScrollable = true, + contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp), + header = { + SpaceAnnouncementHeader(state = state) + }, + content = { + SpaceAnnouncementContent( + state = state, + modifier = Modifier.padding(horizontal = 8.dp), + ) + }, + footer = { + SpaceAnnouncementFooter( + onContinue = ::onContinue, + ) + } + ) +} + +@Composable +private fun SpaceAnnouncementHeader( + state: SpaceAnnouncementState, + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 16.dp, bottom = 16.dp), + title = stringResource(id = R.string.screen_space_announcement_title), + showBetaLabel = true, + subTitle = stringResource( + id = R.string.screen_space_announcement_subtitle, + state.applicationName + ), + iconStyle = BigIcon.Style.Default( + vectorIcon = CompoundIcons.WorkspaceSolid(), + usePrimaryTint = true, + ), + ) +} + +@Composable +private fun SpaceAnnouncementContent( + state: SpaceAnnouncementState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + InfoListOrganism( + modifier = Modifier.fillMaxWidth(), + items = persistentListOf( + InfoListItem( + message = stringResource(id = R.string.screen_space_announcement_item1, state.desktopApplicationName), + iconVector = CompoundIcons.VisibilityOn(), + ), + InfoListItem( + message = stringResource(id = R.string.screen_space_announcement_item2), + iconVector = CompoundIcons.Email(), + ), + InfoListItem( + message = stringResource(id = R.string.screen_space_announcement_item3), + iconVector = CompoundIcons.Search(), + ), + InfoListItem( + message = stringResource(id = R.string.screen_space_announcement_item4), + iconVector = CompoundIcons.Leave(), + ), + InfoListItem( + message = stringResource(id = R.string.screen_space_announcement_item5), + iconVector = CompoundIcons.Explore(), + ), + ), + textStyle = ElementTheme.typography.fontBodyLgMedium, + iconTint = ElementTheme.colors.iconSecondary, + iconSize = 24.dp + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + text = stringResource(id = R.string.screen_space_announcement_notice), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun SpaceAnnouncementFooter( + onContinue: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 8.dp) + ) { + Button( + text = stringResource(id = CommonStrings.action_continue), + onClick = onContinue, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@PreviewsDayNight +@Composable +internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview { + SpaceAnnouncementView( + state = state, + ) +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt new file mode 100644 index 0000000000..dd12120d23 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/AnnouncementStore.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.store + +import kotlinx.coroutines.flow.Flow + +interface AnnouncementStore { + suspend fun setSpaceAnnouncementValue(value: SpaceAnnouncement) + fun spaceAnnouncementFlow(): Flow + + suspend fun reset() + + enum class SpaceAnnouncement { + NeverShown, + Show, + Shown, + } +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt new file mode 100644 index 0000000000..922a4aaaa7 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.store + +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val spaceAnnouncementKey = intPreferencesKey("spaceAnnouncement") + +@ContributesBinding(AppScope::class) +@Inject +class DefaultAnnouncementStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, +) : AnnouncementStore { + private val store = preferenceDataStoreFactory.create("elementx_announcement") + + override suspend fun setSpaceAnnouncementValue(value: AnnouncementStore.SpaceAnnouncement) { + store.edit { + it[spaceAnnouncementKey] = value.ordinal + } + } + + override fun spaceAnnouncementFlow(): Flow { + return store.data.map { prefs -> + val ordinal = prefs[spaceAnnouncementKey] ?: AnnouncementStore.SpaceAnnouncement.NeverShown.ordinal + AnnouncementStore.SpaceAnnouncement.entries.getOrElse(ordinal) { AnnouncementStore.SpaceAnnouncement.NeverShown } + } + } + + override suspend fun reset() { + store.edit { it.clear() } + } +} diff --git a/features/announcement/impl/src/main/res/values/localazy.xml b/features/announcement/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..b995021dfd --- /dev/null +++ b/features/announcement/impl/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "View spaces you’ve created or joined on %1$s desktop" + "Accept or decline invites to spaces" + "Discover any rooms you can join in your spaces" + "Leave any spaces you’ve joined" + "Join public spaces" + "More features will be added in the future, such as creating or managing spaces on mobile." + "Welcome to the beta version of Spaces on %1$s mobile! With this first version you can:" + "Introducing Spaces" + diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt new file mode 100644 index 0000000000..5bfa74bc27 --- /dev/null +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.impl + +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AnnouncementPresenterTest { + @Test + fun `present - initial test`() = runTest { + // TODO + } +} diff --git a/features/announcement/test/build.gradle.kts b/features/announcement/test/build.gradle.kts new file mode 100644 index 0000000000..9387dc0caf --- /dev/null +++ b/features/announcement/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.features.announcement.test" +} + +dependencies { + implementation(projects.features.announcement.api) + implementation(libs.coroutines.core) + implementation(projects.tests.testutils) +} diff --git a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt new file mode 100644 index 0000000000..3d3bbfcaba --- /dev/null +++ b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.test.logs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.features.announcement.api.AnnouncementService +import io.element.android.features.announcement.api.AnnouncementState +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeAnnouncementService( + val onEnteringSpaceTabResult: () -> Unit = { lambdaError() }, + val renderResult: (AnnouncementState, Modifier) -> Unit = { _, _ -> lambdaError() }, +) : AnnouncementService { + override suspend fun onEnteringSpaceTab() { + onEnteringSpaceTabResult() + } + + @Composable + override fun Render(state: AnnouncementState, modifier: Modifier) { + renderResult(state, modifier) + } +} diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 2bf09c9398..9a29532ef0 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.permissions.noop) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) + implementation(projects.features.announcement.api) implementation(projects.features.invite.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.logout.api) @@ -60,6 +61,7 @@ dependencies { api(projects.features.home.api) testCommonDependencies(libs, true) + testImplementation(projects.features.announcement.test) testImplementation(projects.features.invite.test) testImplementation(projects.features.logout.test) testImplementation(projects.features.networkmonitor.test) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index 653a7134f3..d5f3e68898 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject +import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState @@ -47,6 +48,7 @@ class HomePresenter( private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val featureFlagService: FeatureFlagService, private val sessionStore: SessionStore, + private val announcementService: AnnouncementService, ) : Presenter { private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder() @@ -84,7 +86,10 @@ class HomePresenter( fun handleEvents(event: HomeEvents) { when (event) { - is HomeEvents.SelectHomeNavigationBarItem -> { + is HomeEvents.SelectHomeNavigationBarItem -> coroutineState.launch { + if (event.item == HomeNavigationBarItem.Spaces) { + announcementService.onEnteringSpaceTab() + } currentHomeNavigationBarItemOrdinal = event.item.ordinal } is HomeEvents.SwitchToAccount -> coroutineState.launch { diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 8e5b35de99..aa5a612760 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -11,11 +11,13 @@ 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.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.aHomeSpacesState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.features.rageshake.test.logs.FakeAnnouncementService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -37,6 +39,7 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.MutablePresenter import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -165,10 +168,14 @@ class HomePresenterTest { @Test fun `present - NavigationBar change`() = runTest { + val onEnteringSpaceTabResult = lambdaRecorder { } val presenter = createHomePresenter( sessionStore = InMemorySessionStore( updateUserProfileResult = { _, _, _ -> }, ), + announcementService = FakeAnnouncementService( + onEnteringSpaceTabResult = onEnteringSpaceTabResult, + ) ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -178,6 +185,7 @@ class HomePresenterTest { initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) val finalState = awaitItem() assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) + onEnteringSpaceTabResult.assertions().isCalledOnce() } } @@ -192,6 +200,9 @@ class HomePresenterTest { initialState = mapOf(FeatureFlags.Space.key to true), ), homeSpacesPresenter = homeSpacesPresenter, + announcementService = FakeAnnouncementService( + onEnteringSpaceTabResult = {}, + ) ) presenter.test { skipItems(1) @@ -222,6 +233,7 @@ internal fun createHomePresenter( featureFlagService: FeatureFlagService = FakeFeatureFlagService(), homeSpacesPresenter: Presenter = Presenter { aHomeSpacesState() }, sessionStore: SessionStore = InMemorySessionStore(), + announcementService: AnnouncementService = FakeAnnouncementService(), ) = HomePresenter( client = client, syncService = syncService, @@ -233,4 +245,5 @@ internal fun createHomePresenter( rageshakeFeatureAvailability = rageshakeFeatureAvailability, featureFlagService = featureFlagService, sessionStore = sessionStore, + announcementService = announcementService, ) diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 2afebbef4d..73347a5896 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -22,6 +22,12 @@ "settings_rageshake.*" ] }, + { + "name" : ":features:announcement:impl", + "includeRegex" : [ + "screen\\.space_announcement\\..*" + ] + }, { "name" : ":features:logout:impl", "includeRegex" : [ From 48bc32e48b0ba4bf2638b69af5e71ad6043a9da6 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 2 Oct 2025 22:32:40 +0000 Subject: [PATCH 05/17] Update screenshots --- ...announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png | 3 +++ ...nouncement.impl.spaces_SpaceAnnouncementView_Night_0_en.png | 3 +++ .../libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png | 3 +++ ...ibraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png new file mode 100644 index 0000000000..d162a35968 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbaf478329e93e8762e5063e984ca124ab15171950a354324d353cd8454d2d9d +size 65293 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png new file mode 100644 index 0000000000..fef29ced8c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:036f5e862232821552b7a7a25d32de3d65b7de68a4c410231bcf300b8952d6dc +size 64055 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png new file mode 100644 index 0000000000..5b11b1434d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63de7652fe0a3f62ba1cbb00d3a5cb49e620552d8ebb07b9cab916c5ca810538 +size 5175 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png new file mode 100644 index 0000000000..2041b72844 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d71a95f7319fb5c73a456eeae3a0c8bd67c489c460620ad76516f33ca6de364 +size 4972 From 15896c2668869bd0c7411ff2e9d532ca1d1b16aa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 09:40:38 +0200 Subject: [PATCH 06/17] Add unit test on AnnouncementPresenter --- .../impl/AnnouncementPresenterTest.kt | 50 +++++++++++++++++++ .../impl/store/InMemoryAnnouncementStore.kt | 29 +++++++++++ .../impl/AnnouncementPresenterTest.kt | 18 ------- 3 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt create mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt delete mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt new file mode 100644 index 0000000000..87cd68a405 --- /dev/null +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.announcement.impl.store.AnnouncementStore +import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AnnouncementPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createAnnouncementPresenter() + presenter.test { + val state = awaitItem() + assertThat(state.showSpaceAnnouncement).isFalse() + } + } + + @Test + fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest { + val store = InMemoryAnnouncementStore() + val presenter = createAnnouncementPresenter( + announcementStore = store, + ) + presenter.test { + val state = awaitItem() + assertThat(state.showSpaceAnnouncement).isFalse() + store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show) + val updatedState = awaitItem() + assertThat(updatedState.showSpaceAnnouncement).isTrue() + store.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown) + val finalState = awaitItem() + assertThat(finalState.showSpaceAnnouncement).isFalse() + } + } +} + +private fun createAnnouncementPresenter( + announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), +) = AnnouncementPresenter( + announcementStore = announcementStore, +) diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt new file mode 100644 index 0000000000..0f99ef66bd --- /dev/null +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.store + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class InMemoryAnnouncementStore( + initialSpaceAnnouncement: AnnouncementStore.SpaceAnnouncement = AnnouncementStore.SpaceAnnouncement.NeverShown, +) : AnnouncementStore { + private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncement) + override suspend fun setSpaceAnnouncementValue(value: AnnouncementStore.SpaceAnnouncement) { + spaceAnnouncement.value = value + } + + override fun spaceAnnouncementFlow(): Flow { + return spaceAnnouncement.asStateFlow() + } + + override suspend fun reset() { + spaceAnnouncement.value = AnnouncementStore.SpaceAnnouncement.NeverShown + } +} diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt deleted file mode 100644 index 5bfa74bc27..0000000000 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/rageshake/impl/AnnouncementPresenterTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.rageshake.impl - -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class AnnouncementPresenterTest { - @Test - fun `present - initial test`() = runTest { - // TODO - } -} From 831eaca43d04268392c6222cedfad28c206bfe3a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 09:54:42 +0200 Subject: [PATCH 07/17] Add unit test on SpaceAnnouncementPresenter --- features/announcement/impl/build.gradle.kts | 1 + .../spaces/SpaceAnnouncementPresenterTest.kt | 57 +++++++++++++++++++ .../android/libraries/matrix/test/TestData.kt | 1 + 3 files changed, 59 insertions(+) create mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt diff --git a/features/announcement/impl/build.gradle.kts b/features/announcement/impl/build.gradle.kts index 0ef6d09061..e9f653cb4c 100644 --- a/features/announcement/impl/build.gradle.kts +++ b/features/announcement/impl/build.gradle.kts @@ -33,4 +33,5 @@ dependencies { implementation(libs.androidx.datastore.preferences) testCommonDependencies(libs) + testImplementation(projects.libraries.matrix.test) } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt new file mode 100644 index 0000000000..c3ab3a410c --- /dev/null +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.announcement.impl.store.AnnouncementStore +import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME +import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME_DESKTOP +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SpaceAnnouncementPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createSpaceAnnouncementPresenter() + presenter.test { + val state = awaitItem() + assertThat(state.applicationName).isEqualTo(AN_APPLICATION_NAME) + assertThat(state.desktopApplicationName).isEqualTo(AN_APPLICATION_NAME_DESKTOP) + } + } + + @Test + fun `present - when user continues, the store is updated`() = runTest { + val store = InMemoryAnnouncementStore() + val presenter = createSpaceAnnouncementPresenter( + announcementStore = store, + ) + presenter.test { + assertThat(store.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown) + val state = awaitItem() + state.eventSink(SpaceAnnouncementEvents.Continue) + assertThat(store.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown) + } + } +} + +private fun createSpaceAnnouncementPresenter( + buildMeta: BuildMeta = aBuildMeta( + applicationName = AN_APPLICATION_NAME, + desktopApplicationName = AN_APPLICATION_NAME_DESKTOP, + ), + announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), +) = SpaceAnnouncementPresenter( + buildMeta = buildMeta, + announcementStore = announcementStore, +) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index ed18a5ebd9..f05f6958c0 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -26,6 +26,7 @@ const val A_PASSWORD = "password" const val A_PASSPHRASE = "passphrase" const val A_SECRET = "secret" const val AN_APPLICATION_NAME = "AppName" +const val AN_APPLICATION_NAME_DESKTOP = "AppNameDesktop" val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") From c39f1f402fd3c4ae185e6ff5d215de711dfdd427 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 10:00:22 +0200 Subject: [PATCH 08/17] Remove unused Node --- .../impl/spaces/SpaceAnnouncementNode.kt | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt deleted file mode 100644 index ba8f74d803..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementNode.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -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 dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.AssistedInject -import io.element.android.annotations.ContributesNode -import io.element.android.libraries.di.SessionScope - -@ContributesNode(SessionScope::class) -@AssistedInject -class SpaceAnnouncementNode( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: SpaceAnnouncementPresenter, -) : Node(buildContext, plugins = plugins) { - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - SpaceAnnouncementView( - state = state, - modifier = modifier, - ) - } -} From a831412946f60514a4c65384a9319b59ae4c2ee1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 10:11:58 +0200 Subject: [PATCH 09/17] Add test on DefaultAnnouncementService --- .../impl/DefaultAnnouncementService.kt | 5 ++- .../impl/di/AnnouncementModule.kt | 5 +++ .../impl/DefaultAnnouncementServiceTest.kt | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt index fb82246a3c..a5ebb1716a 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -19,16 +19,17 @@ import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.announcement.api.AnnouncementState -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter +import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView import io.element.android.features.announcement.impl.store.AnnouncementStore +import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.flow.first @ContributesBinding(AppScope::class) @Inject class DefaultAnnouncementService( private val announcementStore: AnnouncementStore, - private val spaceAnnouncementPresenter: SpaceAnnouncementPresenter, + private val spaceAnnouncementPresenter: Presenter, ) : AnnouncementService { override suspend fun onEnteringSpaceTab() { val currentValue = announcementStore.spaceAnnouncementFlow().first() diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt index 64998dfe1a..282f653aa7 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt @@ -13,6 +13,8 @@ import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.announcement.impl.AnnouncementPresenter +import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter +import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @@ -20,4 +22,7 @@ import io.element.android.libraries.architecture.Presenter interface AnnouncementModule { @Binds fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter + + @Binds + fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt new file mode 100644 index 0000000000..5882549785 --- /dev/null +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState +import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState +import io.element.android.features.announcement.impl.store.AnnouncementStore +import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultAnnouncementServiceTest { + @Test + fun `when entering space tab, space announcement is set to show only if it was never shown`() = runTest { + val announcementStore = InMemoryAnnouncementStore() + val sut = createDefaultAnnouncementService( + announcementStore = announcementStore, + ) + assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown) + sut.onEnteringSpaceTab() + assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Show) + // Simulate user close the announcement + announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown) + // Entering again the space tab should not change the value + sut.onEnteringSpaceTab() + assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown) + } + + private fun createDefaultAnnouncementService( + announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), + spaceAnnouncementPresenter: Presenter = Presenter { aSpaceAnnouncementState() }, + ) = DefaultAnnouncementService( + announcementStore = announcementStore, + spaceAnnouncementPresenter = spaceAnnouncementPresenter, + ) +} From e8e958137c90ccbca68af12d66d6d3e088f25158 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 10:52:45 +0200 Subject: [PATCH 10/17] Use onContinue method in the back handler --- .../announcement/impl/spaces/SpaceAnnouncementView.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt index 58799c05bd..7f0a7291a5 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt @@ -49,9 +49,7 @@ fun SpaceAnnouncementView( eventSink(SpaceAnnouncementEvents.Continue) } - BackHandler { - state.eventSink(SpaceAnnouncementEvents.Continue) - } + BackHandler(onBack = ::onContinue) HeaderFooterPage( modifier = modifier, isScrollable = true, From 26106365ef48f2e39ec42b89dac2ff2859323958 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 11:35:48 +0200 Subject: [PATCH 11/17] Add test on SpaceAnnouncementView --- features/announcement/impl/build.gradle.kts | 2 +- .../impl/spaces/SpaceAnnouncementViewTest.kt | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt diff --git a/features/announcement/impl/build.gradle.kts b/features/announcement/impl/build.gradle.kts index e9f653cb4c..222d080d6e 100644 --- a/features/announcement/impl/build.gradle.kts +++ b/features/announcement/impl/build.gradle.kts @@ -32,6 +32,6 @@ dependencies { api(projects.features.announcement.api) implementation(libs.androidx.datastore.preferences) - testCommonDependencies(libs) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt new file mode 100644 index 0000000000..96b98668a4 --- /dev/null +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.spaces + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SpaceAnnouncementViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back sends a SpaceAnnouncementEvents`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceAnnouncementView( + aSpaceAnnouncementState( + eventSink = eventsRecorder, + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue) + } + + @Test + fun `clicking on Continue sends a SpaceAnnouncementEvents`() { + val eventsRecorder = EventsRecorder() + rule.setSpaceAnnouncementView( + aSpaceAnnouncementState( + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue) + } +} + +private fun AndroidComposeTestRule.setSpaceAnnouncementView( + state: SpaceAnnouncementState, +) { + setContent { + SpaceAnnouncementView( + state = state, + ) + } +} From 93c273468ea3c40bcdf9ce67b36be90cadf3b79b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 11:57:06 +0200 Subject: [PATCH 12/17] Do not expose `AnnouncementState` in the api module --- .../kotlin/io/element/android/appnav/RootFlowNode.kt | 4 ++-- .../io/element/android/appnav/root/RootPresenter.kt | 4 ---- .../kotlin/io/element/android/appnav/root/RootState.kt | 2 -- .../element/android/appnav/root/RootStateProvider.kt | 2 -- .../kotlin/io/element/android/appnav/root/RootView.kt | 10 +++------- .../io/element/android/appnav/RootPresenterTest.kt | 2 -- .../features/announcement/api/AnnouncementService.kt | 1 - .../announcement/impl/AnnouncementPresenter.kt | 1 - .../features/announcement/impl}/AnnouncementState.kt | 2 +- .../announcement/impl/DefaultAnnouncementService.kt | 7 ++++--- .../announcement/impl/di/AnnouncementModule.kt | 2 +- .../impl/DefaultAnnouncementServiceTest.kt | 2 ++ .../rageshake/test/logs/FakeAnnouncementService.kt | 7 +++---- 13 files changed, 16 insertions(+), 30 deletions(-) rename features/announcement/{api/src/main/kotlin/io/element/android/features/announcement/api => impl/src/main/kotlin/io/element/android/features/announcement/impl}/AnnouncementState.kt (88%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index c01b42af37..4498a44e07 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -174,8 +174,8 @@ class RootFlowNode( state = state, modifier = modifier, onOpenBugReport = this::onOpenBugReport, - announcementRenderer = { state, announcementModifier -> - announcementService.Render(state, announcementModifier) + announcementRenderer = { announcementModifier -> + announcementService.Render(announcementModifier) } ) { val backstackSlider = rememberBackstackSlider( diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt index 928e551a56..d987c2a7ec 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootPresenter.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.SuperProperties -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.libraries.architecture.Presenter @@ -25,7 +24,6 @@ import io.element.android.services.apperror.api.AppErrorStateService class RootPresenter( private val crashDetectionPresenter: Presenter, private val rageshakeDetectionPresenter: Presenter, - private val announcementPresenter: Presenter, private val appErrorStateService: AppErrorStateService, private val analyticsService: AnalyticsService, private val sdkMetadata: SdkMetadata, @@ -34,7 +32,6 @@ class RootPresenter( override fun present(): RootState { val rageshakeDetectionState = rageshakeDetectionPresenter.present() val crashDetectionState = crashDetectionPresenter.present() - val announcementState = announcementPresenter.present() val appErrorState by appErrorStateService.appErrorStateFlow.collectAsState() LaunchedEffect(Unit) { @@ -51,7 +48,6 @@ class RootPresenter( rageshakeDetectionState = rageshakeDetectionState, crashDetectionState = crashDetectionState, errorState = appErrorState, - announcementState = announcementState, ) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt index 5ab995246f..3ea7362efa 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootState.kt @@ -8,7 +8,6 @@ package io.element.android.appnav.root import androidx.compose.runtime.Immutable -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.services.apperror.api.AppErrorState @@ -18,5 +17,4 @@ data class RootState( val rageshakeDetectionState: RageshakeDetectionState, val crashDetectionState: CrashDetectionState, val errorState: AppErrorState, - val announcementState: AnnouncementState, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt index 896e62d820..4d84e06070 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt @@ -8,7 +8,6 @@ package io.element.android.appnav.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.announcement.api.anAnnouncementState import io.element.android.features.rageshake.api.crash.aCrashDetectionState import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState import io.element.android.services.apperror.api.AppErrorState @@ -34,6 +33,5 @@ open class RootStateProvider : PreviewParameterProvider { fun aRootState() = RootState( rageshakeDetectionState = aRageshakeDetectionState(), crashDetectionState = aCrashDetectionState(), - announcementState = anAnnouncementState(), errorState = AppErrorState.NoError, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index 275ccaae84..2913407fc1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.rageshake.api.crash.CrashDetectionEvents import io.element.android.features.rageshake.api.crash.CrashDetectionView import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents @@ -28,7 +27,7 @@ import io.element.android.services.apperror.impl.AppErrorView fun RootView( state: RootState, onOpenBugReport: () -> Unit, - announcementRenderer: @Composable (AnnouncementState, Modifier) -> Unit, + announcementRenderer: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, children: @Composable BoxScope.() -> Unit, ) { @@ -45,10 +44,7 @@ fun RootView( onOpenBugReport.invoke() } - announcementRenderer( - state.announcementState, - Modifier, - ) + announcementRenderer(Modifier) RageshakeDetectionView( state = state.rageshakeDetectionState, @@ -70,7 +66,7 @@ internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootSta RootView( state = rootState, onOpenBugReport = {}, - announcementRenderer = { _, _ -> }, + announcementRenderer = { }, ) { Text("Children") } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 9d0d1e9572..2a343a1592 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -12,7 +12,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.appnav.root.RootPresenter -import io.element.android.features.announcement.api.anAnnouncementState import io.element.android.features.rageshake.api.crash.aCrashDetectionState import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState import io.element.android.libraries.matrix.test.FakeSdkMetadata @@ -72,7 +71,6 @@ class RootPresenterTest { return RootPresenter( crashDetectionPresenter = { aCrashDetectionState() }, rageshakeDetectionPresenter = { aRageshakeDetectionState() }, - announcementPresenter = { anAnnouncementState() }, appErrorStateService = appErrorService, analyticsService = FakeAnalyticsService(), sdkMetadata = FakeSdkMetadata("sha") diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt index abd50aef3b..de822c8873 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt @@ -15,7 +15,6 @@ interface AnnouncementService { @Composable fun Render( - state: AnnouncementState, modifier: Modifier, ) } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt index 435d69d1f5..76746bf44e 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.flow.map diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt similarity index 88% rename from features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt rename to features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt index eccdbd237c..c8ea728d64 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementState.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.announcement.api +package io.element.android.features.announcement.impl data class AnnouncementState( val showSpaceAnnouncement: Boolean, diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt index a5ebb1716a..ef6314b82e 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -18,7 +18,6 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import io.element.android.features.announcement.api.AnnouncementService -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView import io.element.android.features.announcement.impl.store.AnnouncementStore @@ -29,6 +28,7 @@ import kotlinx.coroutines.flow.first @Inject class DefaultAnnouncementService( private val announcementStore: AnnouncementStore, + private val announcementPresenter: Presenter, private val spaceAnnouncementPresenter: Presenter, ) : AnnouncementService { override suspend fun onEnteringSpaceTab() { @@ -39,10 +39,11 @@ class DefaultAnnouncementService( } @Composable - override fun Render(state: AnnouncementState, modifier: Modifier) { + override fun Render(modifier: Modifier) { + val announcementState = announcementPresenter.present() Box(modifier = modifier.fillMaxSize()) { AnimatedVisibility( - visible = state.showSpaceAnnouncement, + visible = announcementState.showSpaceAnnouncement, enter = fadeIn(), exit = fadeOut(), ) { diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt index 282f653aa7..4fbc9118bc 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt @@ -11,8 +11,8 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.features.announcement.impl.AnnouncementPresenter +import io.element.android.features.announcement.impl.AnnouncementState import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState import io.element.android.libraries.architecture.Presenter diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt index 5882549785..4e6249787a 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt @@ -36,9 +36,11 @@ class DefaultAnnouncementServiceTest { private fun createDefaultAnnouncementService( announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), + announcementPresenter: Presenter = Presenter { anAnnouncementState() }, spaceAnnouncementPresenter: Presenter = Presenter { aSpaceAnnouncementState() }, ) = DefaultAnnouncementService( announcementStore = announcementStore, + announcementPresenter = announcementPresenter, spaceAnnouncementPresenter = spaceAnnouncementPresenter, ) } diff --git a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt index 3d3bbfcaba..a9f5452efc 100644 --- a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt +++ b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt @@ -10,19 +10,18 @@ package io.element.android.features.rageshake.test.logs import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.features.announcement.api.AnnouncementService -import io.element.android.features.announcement.api.AnnouncementState import io.element.android.tests.testutils.lambda.lambdaError class FakeAnnouncementService( val onEnteringSpaceTabResult: () -> Unit = { lambdaError() }, - val renderResult: (AnnouncementState, Modifier) -> Unit = { _, _ -> lambdaError() }, + val renderResult: (Modifier) -> Unit = { lambdaError() }, ) : AnnouncementService { override suspend fun onEnteringSpaceTab() { onEnteringSpaceTabResult() } @Composable - override fun Render(state: AnnouncementState, modifier: Modifier) { - renderResult(state, modifier) + override fun Render(modifier: Modifier) { + renderResult(modifier) } } From d7f39c332f2ff65f059d08058ab9ad5daa9ec6e8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 12:03:06 +0200 Subject: [PATCH 13/17] Introduce Announcement enum. --- .../features/announcement/api/Announcement.kt | 12 ++++++++++++ .../features/announcement/api/AnnouncementService.kt | 2 +- .../announcement/impl/DefaultAnnouncementService.kt | 9 ++++++++- .../impl/DefaultAnnouncementServiceTest.kt | 7 ++++--- .../rageshake/test/logs/FakeAnnouncementService.kt | 7 ++++--- .../android/features/home/impl/HomePresenter.kt | 3 ++- .../android/features/home/impl/HomePresenterTest.kt | 11 +++++++---- 7 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt new file mode 100644 index 0000000000..96fd738903 --- /dev/null +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.api + +enum class Announcement { + Space, +} diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt index de822c8873..62944d727e 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier interface AnnouncementService { - suspend fun onEnteringSpaceTab() + suspend fun showAnnouncement(announcement: Announcement) @Composable fun Render( diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt index ef6314b82e..e9b6310544 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject +import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView @@ -31,7 +32,13 @@ class DefaultAnnouncementService( private val announcementPresenter: Presenter, private val spaceAnnouncementPresenter: Presenter, ) : AnnouncementService { - override suspend fun onEnteringSpaceTab() { + override suspend fun showAnnouncement(announcement: Announcement) { + when (announcement) { + Announcement.Space -> showSpaceAnnouncement() + } + } + + private suspend fun showSpaceAnnouncement() { val currentValue = announcementStore.spaceAnnouncementFlow().first() if (currentValue == AnnouncementStore.SpaceAnnouncement.NeverShown) { announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Show) diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt index 4e6249787a..74155ce13b 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt @@ -8,6 +8,7 @@ package io.element.android.features.announcement.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState import io.element.android.features.announcement.impl.store.AnnouncementStore @@ -19,18 +20,18 @@ import org.junit.Test class DefaultAnnouncementServiceTest { @Test - fun `when entering space tab, space announcement is set to show only if it was never shown`() = runTest { + fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest { val announcementStore = InMemoryAnnouncementStore() val sut = createDefaultAnnouncementService( announcementStore = announcementStore, ) assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.NeverShown) - sut.onEnteringSpaceTab() + sut.showAnnouncement(Announcement.Space) assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Show) // Simulate user close the announcement announcementStore.setSpaceAnnouncementValue(AnnouncementStore.SpaceAnnouncement.Shown) // Entering again the space tab should not change the value - sut.onEnteringSpaceTab() + sut.showAnnouncement(Announcement.Space) assertThat(announcementStore.spaceAnnouncementFlow().first()).isEqualTo(AnnouncementStore.SpaceAnnouncement.Shown) } diff --git a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt index a9f5452efc..a9d56e975c 100644 --- a/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt +++ b/features/announcement/test/src/main/kotlin/io/element/android/features/rageshake/test/logs/FakeAnnouncementService.kt @@ -9,15 +9,16 @@ package io.element.android.features.rageshake.test.logs import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.tests.testutils.lambda.lambdaError class FakeAnnouncementService( - val onEnteringSpaceTabResult: () -> Unit = { lambdaError() }, + val showAnnouncementResult: (Announcement) -> Unit = { lambdaError() }, val renderResult: (Modifier) -> Unit = { lambdaError() }, ) : AnnouncementService { - override suspend fun onEnteringSpaceTab() { - onEnteringSpaceTabResult() + override suspend fun showAnnouncement(announcement: Announcement) { + showAnnouncementResult(announcement) } @Composable diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index d5f3e68898..e3ca9612d1 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject +import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState @@ -88,7 +89,7 @@ class HomePresenter( when (event) { is HomeEvents.SelectHomeNavigationBarItem -> coroutineState.launch { if (event.item == HomeNavigationBarItem.Spaces) { - announcementService.onEnteringSpaceTab() + announcementService.showAnnouncement(Announcement.Space) } currentHomeNavigationBarItemOrdinal = event.item.ordinal } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index aa5a612760..8048564e2b 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -11,6 +11,7 @@ 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.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState @@ -40,6 +41,7 @@ import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.MutablePresenter import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -168,13 +170,13 @@ class HomePresenterTest { @Test fun `present - NavigationBar change`() = runTest { - val onEnteringSpaceTabResult = lambdaRecorder { } + val showAnnouncementResult = lambdaRecorder { } val presenter = createHomePresenter( sessionStore = InMemorySessionStore( updateUserProfileResult = { _, _, _ -> }, ), announcementService = FakeAnnouncementService( - onEnteringSpaceTabResult = onEnteringSpaceTabResult, + showAnnouncementResult = showAnnouncementResult, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -185,7 +187,8 @@ class HomePresenterTest { initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) val finalState = awaitItem() assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) - onEnteringSpaceTabResult.assertions().isCalledOnce() + showAnnouncementResult.assertions().isCalledOnce() + .with(value(Announcement.Space)) } } @@ -201,7 +204,7 @@ class HomePresenterTest { ), homeSpacesPresenter = homeSpacesPresenter, announcementService = FakeAnnouncementService( - onEnteringSpaceTabResult = {}, + showAnnouncementResult = {}, ) ) presenter.test { From 555beb1d3745bc8e74fdc267df6b1a3def1d4d37 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 12:09:43 +0200 Subject: [PATCH 14/17] Improve code. --- .../src/main/kotlin/io/element/android/appnav/RootFlowNode.kt | 4 +--- .../main/kotlin/io/element/android/appnav/root/RootView.kt | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 4498a44e07..19290c5f8b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -174,9 +174,6 @@ class RootFlowNode( state = state, modifier = modifier, onOpenBugReport = this::onOpenBugReport, - announcementRenderer = { announcementModifier -> - announcementService.Render(announcementModifier) - } ) { val backstackSlider = rememberBackstackSlider( transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, @@ -192,6 +189,7 @@ class RootFlowNode( } } BackstackView(transitionHandler = transitionHandler) + announcementService.Render(Modifier) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index 2913407fc1..bd7db5e9c2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -27,7 +27,6 @@ import io.element.android.services.apperror.impl.AppErrorView fun RootView( state: RootState, onOpenBugReport: () -> Unit, - announcementRenderer: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, children: @Composable BoxScope.() -> Unit, ) { @@ -44,8 +43,6 @@ fun RootView( onOpenBugReport.invoke() } - announcementRenderer(Modifier) - RageshakeDetectionView( state = state.rageshakeDetectionState, onOpenBugReport = ::onOpenBugReport, @@ -66,7 +63,6 @@ internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootSta RootView( state = rootState, onOpenBugReport = {}, - announcementRenderer = { }, ) { Text("Children") } From 8d6a75e6627ba13280e90789cc1361717ae977f4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 15:07:41 +0200 Subject: [PATCH 15/17] Use semantics colors. --- .../designsystem/atomic/atoms/BetaLabel.kt | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt index 7ef5b5dec5..12a1dd1c80 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/BetaLabel.kt @@ -14,48 +14,31 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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.tokens.generated.internal.DarkColorTokens -import io.element.android.compound.tokens.generated.internal.LightColorTokens import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text -@OptIn(CoreColorToken::class) @Composable fun BetaLabel( modifier: Modifier = Modifier, ) { - val (backgroundColor, borderColor, textColor) = if (ElementTheme.isLightTheme) { - listOf( - LightColorTokens.colorGreen300, - LightColorTokens.colorGreen700, - LightColorTokens.colorGreen900, - ) - } else { - listOf( - DarkColorTokens.colorGreen300, - DarkColorTokens.colorGreen700, - DarkColorTokens.colorGreen900, - ) - } val shape = RoundedCornerShape(size = 6.dp) Text( modifier = modifier .border( width = 1.dp, - color = borderColor, + color = ElementTheme.colors.borderInfoSubtle, shape = shape, ) .background( - color = backgroundColor, + color = ElementTheme.colors.bgInfoSubtle, shape = shape, ) .padding(horizontal = 8.dp, vertical = 4.dp), text = "BETA", style = ElementTheme.typography.fontBodySmMedium, - color = textColor, + color = ElementTheme.colors.textInfoPrimary, ) } From 3af56b64a5769fca0211afc698eabb0c1d39939b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Oct 2025 15:11:55 +0200 Subject: [PATCH 16/17] Space announcement: iterate on wording. --- .../impl/spaces/SpaceAnnouncementPresenter.kt | 4 ---- .../impl/spaces/SpaceAnnouncementState.kt | 2 -- .../spaces/SpaceAnnouncementStateProvider.kt | 4 ---- .../impl/spaces/SpaceAnnouncementView.kt | 16 +++++----------- .../impl/src/main/res/values/localazy.xml | 10 +++++----- .../spaces/SpaceAnnouncementPresenterTest.kt | 19 ------------------- 6 files changed, 10 insertions(+), 45 deletions(-) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt index dbe619a867..05c42b784e 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt @@ -13,12 +13,10 @@ import dev.zacsweers.metro.Inject import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.features.announcement.impl.store.AnnouncementStore.SpaceAnnouncement import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.meta.BuildMeta import kotlinx.coroutines.launch @Inject class SpaceAnnouncementPresenter( - private val buildMeta: BuildMeta, private val announcementStore: AnnouncementStore, ) : Presenter { @Composable @@ -34,8 +32,6 @@ class SpaceAnnouncementPresenter( } return SpaceAnnouncementState( - applicationName = buildMeta.applicationName, - desktopApplicationName = buildMeta.desktopApplicationName, eventSink = ::handleEvents ) } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt index f02519a405..7628ed27ae 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt @@ -8,7 +8,5 @@ package io.element.android.features.announcement.impl.spaces data class SpaceAnnouncementState( - val applicationName: String, - val desktopApplicationName: String, val eventSink: (SpaceAnnouncementEvents) -> Unit ) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt index 0eb2e8ff48..d994edf3d8 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt @@ -17,11 +17,7 @@ open class SpaceAnnouncementStateProvider : PreviewParameterProvider Unit = {}, ) = SpaceAnnouncementState( - applicationName = applicationName, - desktopApplicationName = desktopApplicationName, eventSink = eventSink, ) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt index 7f0a7291a5..2a8c5257aa 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt @@ -55,11 +55,10 @@ fun SpaceAnnouncementView( isScrollable = true, contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp), header = { - SpaceAnnouncementHeader(state = state) + SpaceAnnouncementHeader() }, content = { SpaceAnnouncementContent( - state = state, modifier = Modifier.padding(horizontal = 8.dp), ) }, @@ -73,17 +72,13 @@ fun SpaceAnnouncementView( @Composable private fun SpaceAnnouncementHeader( - state: SpaceAnnouncementState, modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( modifier = modifier.padding(top = 16.dp, bottom = 16.dp), title = stringResource(id = R.string.screen_space_announcement_title), showBetaLabel = true, - subTitle = stringResource( - id = R.string.screen_space_announcement_subtitle, - state.applicationName - ), + subTitle = stringResource(id = R.string.screen_space_announcement_subtitle), iconStyle = BigIcon.Style.Default( vectorIcon = CompoundIcons.WorkspaceSolid(), usePrimaryTint = true, @@ -93,7 +88,6 @@ private fun SpaceAnnouncementHeader( @Composable private fun SpaceAnnouncementContent( - state: SpaceAnnouncementState, modifier: Modifier = Modifier, ) { Column( @@ -103,7 +97,7 @@ private fun SpaceAnnouncementContent( modifier = Modifier.fillMaxWidth(), items = persistentListOf( InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item1, state.desktopApplicationName), + message = stringResource(id = R.string.screen_space_announcement_item1), iconVector = CompoundIcons.VisibilityOn(), ), InfoListItem( @@ -116,11 +110,11 @@ private fun SpaceAnnouncementContent( ), InfoListItem( message = stringResource(id = R.string.screen_space_announcement_item4), - iconVector = CompoundIcons.Leave(), + iconVector = CompoundIcons.Explore(), ), InfoListItem( message = stringResource(id = R.string.screen_space_announcement_item5), - iconVector = CompoundIcons.Explore(), + iconVector = CompoundIcons.Leave(), ), ), textStyle = ElementTheme.typography.fontBodyLgMedium, diff --git a/features/announcement/impl/src/main/res/values/localazy.xml b/features/announcement/impl/src/main/res/values/localazy.xml index b995021dfd..eff394be10 100644 --- a/features/announcement/impl/src/main/res/values/localazy.xml +++ b/features/announcement/impl/src/main/res/values/localazy.xml @@ -1,11 +1,11 @@ - "View spaces you’ve created or joined on %1$s desktop" + "View spaces you\'ve created or joined" "Accept or decline invites to spaces" "Discover any rooms you can join in your spaces" - "Leave any spaces you’ve joined" - "Join public spaces" - "More features will be added in the future, such as creating or managing spaces on mobile." - "Welcome to the beta version of Spaces on %1$s mobile! With this first version you can:" + "Join public spaces" + "Leave any spaces you’ve joined" + "Creating and managing spaces is coming soon." + "Welcome to the beta version of Spaces! With this first version you can:" "Introducing Spaces" diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt index c3ab3a410c..5adb4dccec 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt @@ -10,26 +10,12 @@ package io.element.android.features.announcement.impl.spaces import com.google.common.truth.Truth.assertThat import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME -import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME_DESKTOP -import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test class SpaceAnnouncementPresenterTest { - @Test - fun `present - initial state`() = runTest { - val presenter = createSpaceAnnouncementPresenter() - presenter.test { - val state = awaitItem() - assertThat(state.applicationName).isEqualTo(AN_APPLICATION_NAME) - assertThat(state.desktopApplicationName).isEqualTo(AN_APPLICATION_NAME_DESKTOP) - } - } - @Test fun `present - when user continues, the store is updated`() = runTest { val store = InMemoryAnnouncementStore() @@ -46,12 +32,7 @@ class SpaceAnnouncementPresenterTest { } private fun createSpaceAnnouncementPresenter( - buildMeta: BuildMeta = aBuildMeta( - applicationName = AN_APPLICATION_NAME, - desktopApplicationName = AN_APPLICATION_NAME_DESKTOP, - ), announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), ) = SpaceAnnouncementPresenter( - buildMeta = buildMeta, announcementStore = announcementStore, ) From cfe7d94450d57379b58d423cfcdc673ebde485b8 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 3 Oct 2025 13:27:43 +0000 Subject: [PATCH 17/17] Update screenshots --- ...nnouncement.impl.spaces_SpaceAnnouncementView_Day_0_en.png | 4 ++-- ...ouncement.impl.spaces_SpaceAnnouncementView_Night_0_en.png | 4 ++-- ...libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png | 4 ++-- ...braries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png index d162a35968..e74a44bc85 100644 --- a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbaf478329e93e8762e5063e984ca124ab15171950a354324d353cd8454d2d9d -size 65293 +oid sha256:780559f9a0677b007c93f0c24bef94ab63c89f2d8c124084557c5c8dec0ea3d6 +size 60500 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png index fef29ced8c..3634085549 100644 --- a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:036f5e862232821552b7a7a25d32de3d65b7de68a4c410231bcf300b8952d6dc -size 64055 +oid sha256:97e068729df68971b35727236873cc7954ae5e63890f255948fb4ccd98b3ae2f +size 59572 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png index 5b11b1434d..9957d3f2a8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63de7652fe0a3f62ba1cbb00d3a5cb49e620552d8ebb07b9cab916c5ca810538 -size 5175 +oid sha256:e6453b165bca4b509d7a2e1749e0534f5f12465de3b32bb0513cf377021a57a5 +size 5080 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png index 2041b72844..abea5053c1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d71a95f7319fb5c73a456eeae3a0c8bd67c489c460620ad76516f33ca6de364 -size 4972 +oid sha256:2431e2c666af3353ae85d9a101d8dde1378b4fa3d1828ff23a7f4753f6fb2725 +size 5112