Merge pull request #4140 from element-hq/feature/fga/compound_design_announcement

change(design) : New component Announcement
This commit is contained in:
ganfra
2025-01-15 11:38:43 +01:00
committed by GitHub
33 changed files with 339 additions and 180 deletions

View File

@@ -7,6 +7,7 @@
package io.element.android.features.preferences.impl.notifications
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
@@ -20,7 +21,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.dialogs.ListOption
@@ -132,7 +134,7 @@ private fun NotificationSettingsContentView(
PreferenceText(
icon = CompoundIcons.VoiceCall(),
title = stringResource(id = R.string.full_screen_intent_banner_title),
subtitle = stringResource(R.string.full_screen_intent_banner_message,),
subtitle = stringResource(R.string.full_screen_intent_banner_message),
onClick = {
state.fullScreenIntentPermissionsState.openFullScreenIntentSettings()
}
@@ -247,12 +249,17 @@ private fun InvalidNotificationSettingsView(
showError: Boolean,
onContinueClick: () -> Unit,
onDismissError: () -> Unit,
modifier: Modifier = Modifier,
) {
DialogLikeBannerMolecule(
Announcement(
title = stringResource(R.string.screen_notification_settings_configuration_mismatch),
content = stringResource(R.string.screen_notification_settings_configuration_mismatch_description),
onSubmitClick = onContinueClick,
onDismissClick = null,
description = stringResource(R.string.screen_notification_settings_configuration_mismatch_description),
type = AnnouncementType.Actionable(
onActionClick = onContinueClick,
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = null,
),
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
if (showError) {

View File

@@ -0,0 +1,17 @@
/*
* 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.roomlist.impl.components
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Common padding for RoomList banners.
*/
internal fun Modifier.roomListBannerPadding() = padding(horizontal = 16.dp, vertical = 8.dp)

View File

@@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun ConfirmRecoveryKeyBanner(
@@ -21,12 +23,15 @@ internal fun ConfirmRecoveryKeyBanner(
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
DialogLikeBannerMolecule(
modifier = modifier,
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.confirm_recovery_key_banner_title),
content = stringResource(R.string.confirm_recovery_key_banner_message),
onSubmitClick = onContinueClick,
onDismissClick = onDismissClick,
description = stringResource(R.string.confirm_recovery_key_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onActionClick = onContinueClick,
onDismissClick = onDismissClick,
),
)
}

View File

@@ -8,21 +8,31 @@
package io.element.android.features.roomlist.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun FullScreenIntentPermissionBanner(state: FullScreenIntentPermissionsState) {
DialogLikeBannerMolecule(
fun FullScreenIntentPermissionBanner(
state: FullScreenIntentPermissionsState,
modifier: Modifier = Modifier
) {
Announcement(
title = stringResource(R.string.full_screen_intent_banner_title),
content = stringResource(R.string.full_screen_intent_banner_message),
onDismissClick = state.dismissFullScreenIntentBanner,
onSubmitClick = state.openFullScreenIntentSettings,
description = stringResource(R.string.full_screen_intent_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = state.dismissFullScreenIntentBanner,
onActionClick = state.openFullScreenIntentSettings,
),
modifier = modifier.roomListBannerPadding(),
)
}

View File

@@ -11,7 +11,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -21,13 +22,15 @@ internal fun NativeSlidingSyncMigrationBanner(
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
DialogLikeBannerMolecule(
modifier = modifier,
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_migrate_to_native_sliding_sync_title),
content = stringResource(R.string.banner_migrate_to_native_sliding_sync_description),
actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
onSubmitClick = onContinueClick,
onDismissClick = onDismissClick,
description = stringResource(R.string.banner_migrate_to_native_sliding_sync_description),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action),
onActionClick = onContinueClick,
onDismissClick = onDismissClick,
)
)
}

View File

@@ -139,13 +139,13 @@ private fun EmptyView(
SecurityBannerState.SetUpRecovery -> {
SetUpRecoveryKeyBanner(
onContinueClick = onSetUpRecoveryClick,
onDismissClick = { eventSink(RoomListEvents.DismissBanner) }
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
)
}
SecurityBannerState.RecoveryKeyConfirmation -> {
ConfirmRecoveryKeyBanner(
onContinueClick = onConfirmRecoveryKeyClick,
onDismissClick = { eventSink(RoomListEvents.DismissBanner) }
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
)
}
else -> Unit
@@ -217,7 +217,7 @@ private fun RoomsViewList(
item {
SetUpRecoveryKeyBanner(
onContinueClick = onSetUpRecoveryClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
)
}
}
@@ -225,7 +225,7 @@ private fun RoomsViewList(
item {
ConfirmRecoveryKeyBanner(
onContinueClick = onConfirmRecoveryKeyClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
)
}
}
@@ -233,7 +233,7 @@ private fun RoomsViewList(
item {
NativeSlidingSyncMigrationBanner(
onContinueClick = onMigrateToNativeSlidingSyncClick,
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) }
onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) },
)
}
}

View File

@@ -11,7 +11,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -21,13 +22,15 @@ internal fun SetUpRecoveryKeyBanner(
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
) {
DialogLikeBannerMolecule(
modifier = modifier,
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_set_up_recovery_title),
content = stringResource(R.string.banner_set_up_recovery_content),
actionText = stringResource(R.string.banner_set_up_recovery_submit),
onSubmitClick = onContinueClick,
onDismissClick = onDismissClick,
description = stringResource(R.string.banner_set_up_recovery_content),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_set_up_recovery_submit),
onActionClick = onContinueClick,
onDismissClick = onDismissClick,
),
)
}

View File

@@ -1,97 +0,0 @@
/*
* Copyright 2023, 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.libraries.designsystem.atomic.molecules
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
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.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
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.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun DialogLikeBannerMolecule(
title: String,
content: String,
onSubmitClick: () -> Unit,
onDismissClick: (() -> Unit)?,
modifier: Modifier = Modifier,
actionText: String = stringResource(CommonStrings.action_continue),
) {
Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Surface(
Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Row {
Text(
text = title,
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Start,
)
if (onDismissClick != null) {
Icon(
modifier = Modifier.clickable(onClick = onDismissClick),
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_close)
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = content,
style = ElementTheme.typography.fontBodyMdRegular,
)
Spacer(modifier = Modifier.height(12.dp))
Button(
text = actionText,
size = ButtonSize.Medium,
modifier = Modifier.fillMaxWidth(),
onClick = onSubmitClick,
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun DialogLikeBannerMoleculePreview() = ElementPreview {
DialogLikeBannerMolecule(
title = "Title",
content = "Content",
onSubmitClick = {},
onDismissClick = {}
)
}

View File

@@ -0,0 +1,211 @@
/*
* 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.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
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
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.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.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Announcement component following design system https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2002-2154.
*/
@Composable
fun Announcement(
title: String,
description: String?,
type: AnnouncementType,
modifier: Modifier = Modifier,
) {
when (type) {
is AnnouncementType.Informative -> InformativeAnnouncement(
title = title,
description = description,
isError = type.isCritical,
modifier = modifier,
)
is AnnouncementType.Actionable -> ActionableAnnouncement(
title = title,
description = description,
actionText = type.actionText,
onActionClick = type.onActionClick,
onDismissClick = type.onDismissClick,
modifier = modifier,
)
}
}
@Immutable
sealed interface AnnouncementType {
data class Informative(val isCritical: Boolean = false) : AnnouncementType
data class Actionable(
val actionText: String,
val onActionClick: () -> Unit,
val onDismissClick: (() -> Unit)?,
) : AnnouncementType
}
@Composable
private fun ActionableAnnouncement(
title: String,
description: String?,
actionText: String,
onActionClick: () -> Unit,
onDismissClick: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
AnnouncementSurface(modifier) {
Column {
TitleAndDescription(
title = title,
description = description,
trailingContent = onDismissClick?.let {
{
Icon(
modifier = Modifier.clickable(onClick = onDismissClick),
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_close)
)
}
}
)
Spacer(Modifier.height(16.dp))
Button(
text = actionText,
size = ButtonSize.Medium,
onClick = onActionClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@Composable
private fun InformativeAnnouncement(
title: String,
description: String?,
isError: Boolean,
modifier: Modifier = Modifier,
) {
AnnouncementSurface(modifier = modifier) {
Row {
Icon(
imageVector = if (isError) CompoundIcons.Error() else CompoundIcons.Info(),
tint = if (isError) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconPrimary,
contentDescription = null,
)
Spacer(Modifier.width(12.dp))
TitleAndDescription(
title = title,
description = description,
titleColor = if (isError) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textPrimary,
)
}
}
}
@Composable
private fun TitleAndDescription(
title: String,
description: String?,
modifier: Modifier = Modifier,
titleColor: Color = ElementTheme.colors.textPrimary,
descriptionColor: Color = ElementTheme.colors.textSecondary,
trailingContent: (@Composable () -> Unit)? = null,
) {
Column(modifier = modifier) {
Row {
Text(
text = title,
style = ElementTheme.typography.fontBodyLgMedium,
color = titleColor,
modifier = Modifier.weight(1f),
)
if (trailingContent != null) {
Spacer(Modifier.width(12.dp))
trailingContent()
}
}
if (description != null) {
Spacer(Modifier.height(4.dp))
Text(
text = description,
style = ElementTheme.typography.fontBodyMdRegular,
color = descriptionColor,
)
}
}
}
@Composable
private fun AnnouncementSurface(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(size = 12.dp),
color = ElementTheme.colors.bgSubtleSecondary
) {
Box(modifier = Modifier.padding(16.dp)) {
content()
}
}
}
@PreviewsDayNight
@Composable
internal fun AnnouncementPreview() = ElementPreview {
Column(
verticalArrangement = spacedBy(16.dp),
modifier = Modifier.padding(16.dp)
) {
Announcement(
title = "Headline",
description = "Text description goes here.",
type = AnnouncementType.Informative(isCritical = false),
)
Announcement(
title = "Headline",
description = "Text description goes here.",
type = AnnouncementType.Informative(isCritical = true),
)
Announcement(
title = "Headline",
description = "Text description goes here.",
type = AnnouncementType.Actionable(
actionText = "Label",
onActionClick = {},
onDismissClick = {},
),
)
}
}