Merge pull request #6561 from element-hq/feature/bma/removeSpaceAnnouncement

Remove space announcement
This commit is contained in:
Benoit Marty 2026-04-14 16:58:25 +02:00 committed by GitHub
commit 65f3e74e35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 406 additions and 429 deletions

View file

@ -8,7 +8,13 @@
package io.element.android.features.announcement.api
enum class Announcement {
Space,
NewNotificationSound,
import androidx.compose.runtime.Immutable
@Immutable
sealed interface Announcement {
enum class Fullscreen : Announcement {
Space,
}
data object NewNotificationSound : Announcement
}

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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 io.element.android.features.announcement.api.Announcement
sealed interface AnnouncementEvent {
data class Continue(
val announcement: Announcement.Fullscreen,
) : AnnouncementEvent
}

View file

@ -12,12 +12,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@Inject
class AnnouncementPresenter(
@ -25,13 +29,39 @@ class AnnouncementPresenter(
) : Presenter<AnnouncementState> {
@Composable
override fun present(): AnnouncementState {
val showSpaceAnnouncement by remember {
announcementStore.announcementStatusFlow(Announcement.Space).map {
it == AnnouncementStatus.Show
val coroutineScope = rememberCoroutineScope()
val fullscreenAnnouncementToShow by remember {
combine(
flowOf(Unit),
announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).map {
it == AnnouncementStatus.Show
},
// Add other announcements here when needed
) { _, showFullscreenSpace ->
when {
showFullscreenSpace -> Announcement.Fullscreen.Space
else -> {
null
}
}
}
}.collectAsState(false)
}.collectAsState(null)
fun handle(event: AnnouncementEvent) {
when (event) {
is AnnouncementEvent.Continue -> coroutineScope.launch {
announcementStore.setAnnouncementStatus(
announcement = event.announcement,
status = AnnouncementStatus.Shown,
)
}
}
}
return AnnouncementState(
showSpaceAnnouncement = showSpaceAnnouncement,
announcement = fullscreenAnnouncementToShow,
eventSink = ::handle,
)
}
}

View file

@ -8,12 +8,9 @@
package io.element.android.features.announcement.impl
data class AnnouncementState(
val showSpaceAnnouncement: Boolean,
)
import io.element.android.features.announcement.api.Announcement
fun anAnnouncementState(
showSpaceAnnouncement: Boolean = false,
) = AnnouncementState(
showSpaceAnnouncement = showSpaceAnnouncement,
data class AnnouncementState(
val announcement: Announcement.Fullscreen?,
val eventSink: (AnnouncementEvent) -> Unit,
)

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.announcement.api.Announcement
open class AnnouncementStateProvider : PreviewParameterProvider<AnnouncementState> {
override val values: Sequence<AnnouncementState>
get() = sequenceOf(
anAnnouncementState(),
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
),
)
}
fun anAnnouncementState(
announcement: Announcement.Fullscreen? = null,
eventSink: (AnnouncementEvent) -> Unit = {},
) = AnnouncementState(
announcement = announcement,
eventSink = eventSink,
)

View file

@ -8,35 +8,28 @@
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 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
import io.element.android.features.announcement.impl.fullscreen.FullscreenAnnouncementView
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultAnnouncementService(
private val announcementStore: AnnouncementStore,
private val announcementPresenter: Presenter<AnnouncementState>,
private val spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState>,
private val announcementPresenter: AnnouncementPresenter,
) : AnnouncementService {
override suspend fun showAnnouncement(announcement: Announcement) {
when (announcement) {
Announcement.Space -> showSpaceAnnouncement()
is Announcement.Fullscreen -> showFullscreenAnnouncement(announcement)
Announcement.NewNotificationSound -> {
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
}
@ -49,13 +42,10 @@ class DefaultAnnouncementService(
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
return combine(
announcementStore.announcementStatusFlow(Announcement.Space),
flowOf(Unit),
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
) { _, newNotificationSoundStatus ->
buildList {
if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
add(Announcement.Space)
}
if (newNotificationSoundStatus == AnnouncementStatus.Show) {
add(Announcement.NewNotificationSound)
}
@ -63,27 +53,19 @@ class DefaultAnnouncementService(
}
}
private suspend fun showSpaceAnnouncement() {
val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
private suspend fun showFullscreenAnnouncement(announcement: Announcement.Fullscreen) {
val currentValue = announcementStore.announcementStatusFlow(announcement).first()
if (currentValue == AnnouncementStatus.NeverShown) {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Show)
}
}
@Composable
override fun Render(modifier: Modifier) {
val announcementState = announcementPresenter.present()
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = announcementState.showSpaceAnnouncement,
enter = fadeIn(),
exit = fadeOut(),
) {
val spaceAnnouncementState = spaceAnnouncementPresenter.present()
SpaceAnnouncementView(
state = spaceAnnouncementState,
)
}
}
FullscreenAnnouncementView(
state = announcementState,
modifier = modifier,
)
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.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
@ContributesTo(AppScope::class)
@BindingContainer
interface AnnouncementModule {
@Binds
fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter<AnnouncementState>
@Binds
fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter<SpaceAnnouncementState>
}

View file

@ -0,0 +1,225 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.fullscreen
import androidx.activity.compose.BackHandler
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.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.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.AnnouncementStateProvider
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.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
*/
@Composable
fun FullscreenAnnouncementView(
state: AnnouncementState,
modifier: Modifier = Modifier,
) {
// Ensure that the content stays visible during the exit animation
var fullscreenAnnouncement by remember { mutableStateOf<Announcement.Fullscreen?>(null) }
if (state.announcement != null) {
fullscreenAnnouncement = state.announcement
}
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = state.announcement != null,
enter = fadeIn(),
exit = fadeOut(),
) {
fullscreenAnnouncement?.let {
FullscreenAnnouncementView(
announcement = it,
eventSink = state.eventSink,
)
}
}
}
}
@Composable
private fun FullscreenAnnouncementView(
announcement: Announcement.Fullscreen,
eventSink: (AnnouncementEvent) -> Unit,
modifier: Modifier = Modifier
) {
fun onContinue() {
eventSink(AnnouncementEvent.Continue(announcement))
}
BackHandler(onBack = ::onContinue)
HeaderFooterPage(
modifier = modifier,
isScrollable = true,
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
header = {
FullscreenAnnouncementHeader(announcement)
},
content = {
FullscreenAnnouncementContent(
modifier = Modifier.padding(horizontal = 8.dp),
announcement = announcement,
)
},
footer = {
FullscreenAnnouncementFooter(
onContinue = ::onContinue,
)
}
)
}
@Composable
private fun FullscreenAnnouncementHeader(
announcement: Announcement.Fullscreen,
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
title = announcement.title(),
showBetaLabel = true,
subTitle = announcement.subtitle(),
iconStyle = BigIcon.Style.Default(
vectorIcon = announcement.icon(),
usePrimaryTint = true,
),
)
}
@Composable
private fun FullscreenAnnouncementContent(
announcement: Announcement.Fullscreen,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
InfoListOrganism(
modifier = Modifier.fillMaxWidth(),
items = announcement.items(),
textStyle = ElementTheme.typography.fontBodyLgMedium,
iconTint = ElementTheme.colors.iconSecondary,
iconSize = 24.dp
)
announcement.notice()?.let { notice ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
text = notice,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
}
@Composable
private fun FullscreenAnnouncementFooter(
onContinue: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 8.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun Announcement.Fullscreen.title() = when (this) {
Announcement.Fullscreen.Space -> "Introducing Spaces"
}
@Composable
private fun Announcement.Fullscreen.subtitle() = when (this) {
Announcement.Fullscreen.Space -> "Welcome to the beta version of Spaces! With this first version you can:"
}
@Composable
private fun Announcement.Fullscreen.icon() = when (this) {
Announcement.Fullscreen.Space -> CompoundIcons.SpaceSolid()
}
@Composable
private fun Announcement.Fullscreen.items(): ImmutableList<InfoListItem> = when (this) {
Announcement.Fullscreen.Space -> persistentListOf(
InfoListItem(
message = "View spaces you\'ve created or joined",
iconVector = CompoundIcons.VisibilityOn(),
),
InfoListItem(
message = "Accept or decline invites to spaces",
iconVector = CompoundIcons.Email(),
),
InfoListItem(
message = "Discover any rooms you can join in your spaces",
iconVector = CompoundIcons.Search(),
),
InfoListItem(
message = "Join public spaces",
iconVector = CompoundIcons.Explore(),
),
InfoListItem(
message = "Leave any spaces youve joined",
iconVector = CompoundIcons.Leave(),
),
)
}
@Composable
private fun Announcement.Fullscreen.notice(): String? = when (this) {
Announcement.Fullscreen.Space -> "Filtering, creating and managing spaces is coming soon."
}
@PreviewsDayNight
@Composable
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview {
FullscreenAnnouncementView(
state = state,
)
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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
}

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
@Inject
class SpaceAnnouncementPresenter(
private val announcementStore: AnnouncementStore,
) : Presenter<SpaceAnnouncementState> {
@Composable
override fun present(): SpaceAnnouncementState {
val localCoroutineScope = rememberCoroutineScope()
fun handleEvent(event: SpaceAnnouncementEvents) {
when (event) {
SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
}
}
}
return SpaceAnnouncementState(
eventSink = ::handleEvent,
)
}
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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 eventSink: (SpaceAnnouncementEvents) -> Unit
)

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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<SpaceAnnouncementState> {
override val values: Sequence<SpaceAnnouncementState>
get() = sequenceOf(
aSpaceAnnouncementState(),
)
}
fun aSpaceAnnouncementState(
eventSink: (SpaceAnnouncementEvents) -> Unit = {},
) = SpaceAnnouncementState(
eventSink = eventSink,
)

View file

@ -1,158 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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(onBack = ::onContinue)
HeaderFooterPage(
modifier = modifier,
isScrollable = true,
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
header = {
SpaceAnnouncementHeader()
},
content = {
SpaceAnnouncementContent(
modifier = Modifier.padding(horizontal = 8.dp),
)
},
footer = {
SpaceAnnouncementFooter(
onContinue = ::onContinue,
)
}
)
}
@Composable
private fun SpaceAnnouncementHeader(
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),
iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.SpaceSolid(),
usePrimaryTint = true,
),
)
}
@Composable
private fun SpaceAnnouncementContent(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
InfoListOrganism(
modifier = Modifier.fillMaxWidth(),
items = persistentListOf(
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item1),
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.Explore(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item5),
iconVector = CompoundIcons.Leave(),
),
),
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,
)
}

View file

@ -35,9 +35,10 @@ class DefaultAnnouncementStore(
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
val key = announcement.toKey()
// Announcement.Fullscreen.Space is disabled, consider it's shown
// For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
val defaultStatus = when (announcement) {
Announcement.Space -> AnnouncementStatus.NeverShown
Announcement.Fullscreen.Space -> AnnouncementStatus.Shown
Announcement.NewNotificationSound -> AnnouncementStatus.Shown
}
return store.data.map { prefs ->
@ -52,6 +53,6 @@ class DefaultAnnouncementStore(
}
private fun Announcement.toKey() = when (this) {
Announcement.Space -> spaceAnnouncementKey
Announcement.Fullscreen.Space -> spaceAnnouncementKey
Announcement.NewNotificationSound -> newNotificationSoundKey
}

View file

@ -14,6 +14,7 @@ import io.element.android.features.announcement.impl.store.AnnouncementStatus
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.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -23,25 +24,47 @@ class AnnouncementPresenterTest {
val presenter = createAnnouncementPresenter()
presenter.test {
val state = awaitItem()
assertThat(state.showSpaceAnnouncement).isFalse()
assertThat(state.announcement).isNull()
}
}
@Test
fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest {
fun `present - showFullscreen 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.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
assertThat(state.announcement).isNull()
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.showSpaceAnnouncement).isTrue()
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space)
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.showSpaceAnnouncement).isFalse()
assertThat(finalState.announcement).isNull()
}
}
@Test
fun `present - continue event will mark the announcement as Shown`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
val state = awaitItem()
assertThat(state.announcement).isNull()
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show)
val statusShow = store.announcementStatusFlow(Announcement.Fullscreen.Space).first()
assertThat(statusShow).isEqualTo(AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space)
updatedState.eventSink(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
val statusShown = store.announcementStatusFlow(Announcement.Fullscreen.Space).first()
assertThat(statusShown).isEqualTo(AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.announcement).isNull()
}
}
}

View file

@ -11,31 +11,28 @@ package io.element.android.features.announcement.impl
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.impl.spaces.SpaceAnnouncementState
import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
import io.element.android.features.announcement.impl.store.AnnouncementStatus
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 showing Space announcement, space announcement is set to show only if it was never shown`() = runTest {
fun `when showing Fullscreen announcement, Fullscreen announcement is set to show only if it was never shown`() = runTest {
val announcementStore = InMemoryAnnouncementStore()
val sut = createDefaultAnnouncementService(
announcementStore = announcementStore,
)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Fullscreen.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Show)
// Simulate user close the announcement
sut.onAnnouncementDismissed(Announcement.Space)
sut.onAnnouncementDismissed(Announcement.Fullscreen.Space)
// Entering again the space tab should not change the value
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
sut.showAnnouncement(Announcement.Fullscreen.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Shown)
}
@Test
@ -62,11 +59,7 @@ class DefaultAnnouncementServiceTest {
)
sut.announcementsToShowFlow().test {
assertThat(awaitItem()).isEmpty()
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
assertThat(awaitItem()).containsExactly(Announcement.Space)
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
assertThat(awaitItem()).isEmpty()
@ -75,11 +68,9 @@ class DefaultAnnouncementServiceTest {
private fun createDefaultAnnouncementService(
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
announcementPresenter: Presenter<AnnouncementState> = Presenter { anAnnouncementState() },
spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState> = Presenter { aSpaceAnnouncementState() },
announcementPresenter: AnnouncementPresenter = AnnouncementPresenter(announcementStore),
) = DefaultAnnouncementService(
announcementStore = announcementStore,
announcementPresenter = announcementPresenter,
spaceAnnouncementPresenter = spaceAnnouncementPresenter,
)
}

View file

@ -6,12 +6,16 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
package io.element.android.features.announcement.impl.fullscreen
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.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.anAnnouncementState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
@ -22,39 +26,41 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SpaceAnnouncementViewTest {
class FullscreenAnnouncementViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back sends a SpaceAnnouncementEvents`() {
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
rule.setSpaceAnnouncementView(
aSpaceAnnouncementState(
fun `clicking on back sends a AnnouncementEvent`() {
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView(
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder,
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
}
@Test
fun `clicking on Continue sends a SpaceAnnouncementEvents`() {
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
rule.setSpaceAnnouncementView(
aSpaceAnnouncementState(
fun `clicking on Continue sends a AnnouncementEvent`() {
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView(
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceAnnouncementView(
state: SpaceAnnouncementState,
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setFullscreenAnnouncementView(
state: AnnouncementState,
) {
setContent {
SpaceAnnouncementView(
FullscreenAnnouncementView(
state = state,
)
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
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.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SpaceAnnouncementPresenterTest {
@Test
fun `present - when user continues, the store is updated`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createSpaceAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
val state = awaitItem()
state.eventSink(SpaceAnnouncementEvents.Continue)
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
}
}
}
private fun createSpaceAnnouncementPresenter(
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
) = SpaceAnnouncementPresenter(
announcementStore = announcementStore,
)

View file

@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class InMemoryAnnouncementStore(
initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
) : AnnouncementStore {
private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus)
private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
@ -29,12 +29,12 @@ class InMemoryAnnouncementStore(
}
override suspend fun reset() {
spaceAnnouncement.value = AnnouncementStatus.NeverShown
fullScreenAnnouncement.value = AnnouncementStatus.NeverShown
newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
}
private fun Announcement.toMutableStateFlow() = when (this) {
Announcement.Space -> spaceAnnouncement
is Announcement.Fullscreen -> fullScreenAnnouncement
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
}
}

View file

@ -19,8 +19,6 @@ 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
import io.element.android.features.logout.api.direct.DirectLogoutState
@ -47,7 +45,6 @@ class HomePresenter(
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val sessionStore: SessionStore,
private val announcementService: AnnouncementService,
) : Presenter<HomeState> {
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
@ -82,10 +79,7 @@ class HomePresenter(
fun handleEvent(event: HomeEvent) {
when (event) {
is HomeEvent.SelectHomeNavigationBarItem -> coroutineState.launch {
if (event.item == HomeNavigationBarItem.Spaces) {
announcementService.showAnnouncement(Announcement.Space)
}
is HomeEvent.SelectHomeNavigationBarItem -> {
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
is HomeEvent.SwitchToAccount -> coroutineState.launch {

View file

@ -9,14 +9,11 @@
package io.element.android.features.home.impl
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
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.indicator.api.IndicatorService
@ -34,8 +31,6 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
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
@ -137,14 +132,10 @@ class HomePresenterTest {
@Test
fun `present - NavigationBar change`() = runTest {
val showAnnouncementResult = lambdaRecorder<Announcement, Unit> { }
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
announcementService = FakeAnnouncementService(
showAnnouncementResult = showAnnouncementResult,
)
)
presenter.test {
val initialState = awaitItem()
@ -152,8 +143,6 @@ class HomePresenterTest {
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val finalState = awaitItem()
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
showAnnouncementResult.assertions().isCalledOnce()
.with(value(Announcement.Space))
}
}
}
@ -166,7 +155,6 @@ internal fun createHomePresenter(
indicatorService: IndicatorService = FakeIndicatorService(),
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
sessionStore: SessionStore = InMemorySessionStore(),
announcementService: AnnouncementService = FakeAnnouncementService(),
) = HomePresenter(
client = client,
syncService = syncService,
@ -177,5 +165,4 @@ internal fun createHomePresenter(
logoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
sessionStore = sessionStore,
announcementService = announcementService,
)

View file

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

View file

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