Add 'unencrypted room' badges and labels (#4445)

* Add 'unencrypted room' icon and label to composer

* Modify colors for room details screen info labels

* Add exception to Konsist's preview check

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2025-03-25 12:26:25 +01:00
committed by GitHub
parent 56683259d9
commit bb97015e59
113 changed files with 493 additions and 196 deletions

View File

@@ -73,7 +73,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false)
val textEditorState by rememberUpdatedState(
TextEditorState.Markdown(markdownTextEditorState)
TextEditorState.Markdown(markdownTextEditorState, isRoomEncrypted = null)
)
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }

View File

@@ -141,6 +141,8 @@ class MessageComposerPresenter @AssistedInject constructor(
override fun present(): MessageComposerState {
val localCoroutineScope = rememberCoroutineScope()
val roomInfo by room.roomInfoFlow.collectAsState()
val richTextEditorState = richTextEditorStateFactory.remember()
if (isTesting) {
richTextEditorState.isReadyToProcessActions = true
@@ -242,9 +244,9 @@ class MessageComposerPresenter @AssistedInject constructor(
val textEditorState by rememberUpdatedState(
if (showTextFormatting) {
TextEditorState.Rich(richTextEditorState)
TextEditorState.Rich(richTextEditorState, roomInfo.isEncrypted == true)
} else {
TextEditorState.Markdown(markdownTextEditorState)
TextEditorState.Markdown(markdownTextEditorState, roomInfo.isEncrypted == true)
}
)

View File

@@ -469,14 +469,14 @@ private fun RoomBadge.toMatrixBadgeData(): MatrixBadgeAtom.MatrixBadgeData {
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(R.string.screen_room_details_badge_not_encrypted),
icon = CompoundIcons.LockOff(),
type = MatrixBadgeAtom.Type.Neutral,
type = MatrixBadgeAtom.Type.Info,
)
}
RoomBadge.PUBLIC -> {
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(R.string.screen_room_details_badge_public),
icon = CompoundIcons.Public(),
type = MatrixBadgeAtom.Type.Neutral,
type = MatrixBadgeAtom.Type.Info,
)
}
}

View File

@@ -14,6 +14,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.Badge
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.badgeInfoBackgroundColor
import io.element.android.libraries.designsystem.theme.badgeInfoContentColor
import io.element.android.libraries.designsystem.theme.badgeNegativeBackgroundColor
import io.element.android.libraries.designsystem.theme.badgeNegativeContentColor
import io.element.android.libraries.designsystem.theme.badgeNeutralBackgroundColor
@@ -31,7 +33,8 @@ object MatrixBadgeAtom {
enum class Type {
Positive,
Neutral,
Negative
Negative,
Info,
}
@Composable
@@ -42,16 +45,19 @@ object MatrixBadgeAtom {
Type.Positive -> ElementTheme.colors.badgePositiveBackgroundColor
Type.Neutral -> ElementTheme.colors.badgeNeutralBackgroundColor
Type.Negative -> ElementTheme.colors.badgeNegativeBackgroundColor
Type.Info -> ElementTheme.colors.badgeInfoBackgroundColor
}
val textColor = when (data.type) {
Type.Positive -> ElementTheme.colors.badgePositiveContentColor
Type.Neutral -> ElementTheme.colors.badgeNeutralContentColor
Type.Negative -> ElementTheme.colors.badgeNegativeContentColor
Type.Info -> ElementTheme.colors.badgeInfoContentColor
}
val iconColor = when (data.type) {
Type.Positive -> ElementTheme.colors.iconSuccessPrimary
Type.Neutral -> ElementTheme.colors.iconSecondary
Type.Negative -> ElementTheme.colors.iconCriticalPrimary
Type.Info -> ElementTheme.colors.iconInfoPrimary
}
Badge(
text = data.text,
@@ -98,3 +104,15 @@ internal fun MatrixBadgeAtomNegativePreview() = ElementPreview {
)
)
}
@PreviewsDayNight
@Composable
internal fun MatrixBadgeAtomInfoPreview() = ElementPreview {
MatrixBadgeAtom.View(
MatrixBadgeAtom.MatrixBadgeData(
text = "Not encrypted",
icon = CompoundIcons.LockOff(),
type = MatrixBadgeAtom.Type.Info,
)
)
}

View File

@@ -165,6 +165,14 @@ val SemanticColors.badgeNegativeBackgroundColor
val SemanticColors.badgeNegativeContentColor
get() = if (isLight) LightColorTokens.colorRed1100 else DarkColorTokens.colorRed1100
@OptIn(CoreColorToken::class)
val SemanticColors.badgeInfoBackgroundColor
get() = if (isLight) LightColorTokens.colorAlphaBlue300 else DarkColorTokens.colorAlphaBlue300
@OptIn(CoreColorToken::class)
val SemanticColors.badgeInfoContentColor
get() = if (isLight) LightColorTokens.colorBlue1100 else DarkColorTokens.colorBlue1100
@OptIn(CoreColorToken::class)
val SemanticColors.pinnedMessageBannerIndicator
get() = if (isLight) LightColorTokens.colorAlphaGray600 else DarkColorTokens.colorAlphaGray600

View File

@@ -7,13 +7,13 @@
package io.element.android.libraries.textcomposer
import android.content.res.Configuration
import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
@@ -37,14 +37,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
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.libraries.designsystem.components.media.createFakeWaveform
import io.element.android.libraries.designsystem.preview.DAY_MODE_NAME
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.NIGHT_MODE_NAME
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconColorButton
import io.element.android.libraries.designsystem.theme.components.Text
@@ -281,6 +285,7 @@ fun TextComposer(
if (showTextFormatting && textFormattingOptions != null) {
TextFormattingLayout(
modifier = layoutModifier,
isRoomEncrypted = state.isRoomEncrypted,
textInput = textInput,
dismissTextFormattingButton = {
IconColorButton(
@@ -295,6 +300,7 @@ fun TextComposer(
StandardLayout(
voiceMessageState = voiceMessageState,
enableVoiceMessages = enableVoiceMessages,
isRoomEncrypted = state.isRoomEncrypted,
modifier = layoutModifier,
composerOptionsButton = composerOptionsButton,
textInput = textInput,
@@ -330,6 +336,7 @@ fun TextComposer(
private fun StandardLayout(
voiceMessageState: VoiceMessageState,
enableVoiceMessages: Boolean,
isRoomEncrypted: Boolean?,
textInput: @Composable () -> Unit,
composerOptionsButton: @Composable () -> Unit,
voiceRecording: @Composable () -> Unit,
@@ -337,58 +344,85 @@ private fun StandardLayout(
endButton: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.Bottom,
) {
if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) {
if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) {
Column(modifier = modifier) {
if (isRoomEncrypted == false) {
Spacer(Modifier.height(16.dp))
NotEncryptedBadge()
Spacer(Modifier.height(4.dp))
}
Row(verticalAlignment = Alignment.Bottom) {
if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) {
if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) {
Box(
modifier = Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp)
.size(48.dp),
contentAlignment = Alignment.Center,
) {
voiceDeleteButton()
}
} else {
Spacer(modifier = Modifier.width(16.dp))
}
Box(
modifier = Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp)
.size(48.dp),
contentAlignment = Alignment.Center,
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
) {
voiceDeleteButton()
voiceRecording()
}
} else {
Spacer(modifier = Modifier.width(16.dp))
Box(
Modifier
.padding(bottom = 5.dp, top = 5.dp, start = 3.dp)
) {
composerOptionsButton()
}
Box(
modifier = Modifier
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
) {
textInput()
}
}
Box(
modifier = Modifier
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
) {
voiceRecording()
}
} else {
Box(
Modifier
.padding(bottom = 5.dp, top = 5.dp, start = 3.dp)
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp),
contentAlignment = Alignment.Center,
) {
composerOptionsButton()
endButton()
}
Box(
modifier = Modifier
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
) {
textInput()
}
}
Box(
Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp),
contentAlignment = Alignment.Center,
) {
endButton()
}
}
}
@Composable
private fun NotEncryptedBadge() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.LockOff(),
contentDescription = null,
tint = ElementTheme.colors.iconInfoPrimary,
)
Spacer(Modifier.width(4.dp))
Text(
text = stringResource(CommonStrings.common_not_encrypted),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
}
}
@Composable
private fun TextFormattingLayout(
isRoomEncrypted: Boolean?,
textInput: @Composable () -> Unit,
dismissTextFormattingButton: @Composable () -> Unit,
textFormatting: @Composable () -> Unit,
@@ -399,6 +433,10 @@ private fun TextFormattingLayout(
modifier = modifier.padding(vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (isRoomEncrypted == false) {
NotEncryptedBadge()
Spacer(Modifier.height(8.dp))
}
Box(
modifier = Modifier
.weight(1f)
@@ -438,7 +476,7 @@ private fun TextInputBox(
placeholder: String,
showPlaceholder: Boolean,
subcomposing: Boolean,
textInput: @Composable BoxScope.() -> Unit,
textInput: @Composable () -> Unit,
) {
val bgColor = ElementTheme.colors.bgSubtleSecondary
val borderColor = ElementTheme.colors.borderDisabled
@@ -539,24 +577,26 @@ private fun TextInput(
}
}
private fun aTextEditorStateMarkdownList() = persistentListOf(
aTextEditorStateMarkdown(initialText = "", initialFocus = true),
aTextEditorStateMarkdown(initialText = "A message", initialFocus = true),
private fun aTextEditorStateMarkdownList(isRoomEncrypted: Boolean? = null) = persistentListOf(
aTextEditorStateMarkdown(initialText = "", initialFocus = true, isRoomEncrypted = isRoomEncrypted),
aTextEditorStateMarkdown(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted),
aTextEditorStateMarkdown(
initialText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
initialFocus = true,
isRoomEncrypted = isRoomEncrypted,
),
aTextEditorStateMarkdown(initialText = "A message without focus", initialFocus = false),
aTextEditorStateMarkdown(initialText = "A message without focus", initialFocus = false, isRoomEncrypted = isRoomEncrypted),
)
private fun aTextEditorStateRichList() = persistentListOf(
aTextEditorStateRich(initialFocus = true),
aTextEditorStateRich(initialText = "A message", initialFocus = true),
private fun aTextEditorStateRichList(isRoomEncrypted: Boolean? = null) = persistentListOf(
aTextEditorStateRich(initialFocus = true, isRoomEncrypted = isRoomEncrypted),
aTextEditorStateRich(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted),
aTextEditorStateRich(
initialText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
initialFocus = true
initialFocus = true,
isRoomEncrypted = isRoomEncrypted,
),
aTextEditorStateRich(initialText = "A message without focus", initialFocus = false),
aTextEditorStateRich(initialText = "A message without focus", initialFocus = false, isRoomEncrypted = isRoomEncrypted),
)
@PreviewsDayNight
@@ -574,6 +614,21 @@ internal fun TextComposerSimplePreview() = ElementPreview {
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerSimpleNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateMarkdownList(isRoomEncrypted = false),
) { _, textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerFormattingPreview() = ElementPreview {
@@ -590,6 +645,22 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerFormattingNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList(isRoomEncrypted = false)
) { _, textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerEditPreview() = ElementPreview {
@@ -605,6 +676,21 @@ internal fun TextComposerEditPreview() = ElementPreview {
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerEditNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList(isRoomEncrypted = false)
) { _, textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeEdit(),
enableVoiceMessages = true,
)
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerEditCaptionPreview() = ElementPreview {
@@ -674,6 +760,31 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider
}
}
@Preview(
name = DAY_MODE_NAME,
heightDp = 800,
)
@Preview(
name = NIGHT_MODE_NAME,
uiMode = Configuration.UI_MODE_NIGHT_YES,
heightDp = 800,
)
@Composable
internal fun TextComposerReplyNotEncryptedPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList(isRoomEncrypted = false)
) { _, textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeReply(
replyToDetails = inReplyToDetails,
),
enableVoiceMessages = true,
)
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerCaptionPreview() = ElementPreview {
@@ -734,6 +845,47 @@ internal fun TextComposerVoicePreview() = ElementPreview {
}
}
@PreviewsDayNight
@Composable
internal fun TextComposerVoiceNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = persistentListOf(
VoiceMessageState.Recording(61.seconds, createFakeWaveform()),
VoiceMessageState.Preview(
isSending = false,
isPlaying = false,
showCursor = false,
waveform = createFakeWaveform(),
time = 0.seconds,
playbackProgress = 0.0f
),
VoiceMessageState.Preview(
isSending = false,
isPlaying = true,
showCursor = true,
waveform = createFakeWaveform(),
time = 3.seconds,
playbackProgress = 0.2f
),
VoiceMessageState.Preview(
isSending = true,
isPlaying = false,
showCursor = false,
waveform = createFakeWaveform(),
time = 61.seconds,
playbackProgress = 0.0f
),
)
) { _, voiceMessageState ->
ATextComposer(
state = aTextEditorStateRich(initialFocus = true, isRoomEncrypted = false),
voiceMessageState = voiceMessageState,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@Composable
private fun <T> PreviewColumn(
items: ImmutableList<T>,
@@ -741,6 +893,7 @@ private fun <T> PreviewColumn(
) {
Column {
items.forEachIndexed { index, item ->
HorizontalDivider()
Box(
modifier = Modifier.height(IntrinsicSize.Min)
) {

View File

@@ -12,12 +12,14 @@ import io.element.android.wysiwyg.compose.RichTextEditorState
fun aTextEditorStateMarkdown(
initialText: String? = "",
initialFocus: Boolean = false,
isRoomEncrypted: Boolean? = null,
): TextEditorState {
return TextEditorState.Markdown(
aMarkdownTextEditorState(
initialText = initialText,
initialFocus = initialFocus,
)
),
isRoomEncrypted = isRoomEncrypted,
)
}
@@ -36,6 +38,7 @@ fun aTextEditorStateRich(
initialHtml: String = initialText,
initialMarkdown: String = initialText,
initialFocus: Boolean = false,
isRoomEncrypted: Boolean? = null,
): TextEditorState {
return TextEditorState.Rich(
aRichTextEditorState(
@@ -43,7 +46,8 @@ fun aTextEditorStateRich(
initialHtml = initialHtml,
initialMarkdown = initialMarkdown,
initialFocus = initialFocus,
)
),
isRoomEncrypted = isRoomEncrypted,
)
}

View File

@@ -13,12 +13,16 @@ import io.element.android.wysiwyg.compose.RichTextEditorState
@Immutable
sealed interface TextEditorState {
val isRoomEncrypted: Boolean?
data class Markdown(
val state: MarkdownTextEditorState,
override val isRoomEncrypted: Boolean?,
) : TextEditorState
data class Rich(
val richTextEditorState: RichTextEditorState
val richTextEditorState: RichTextEditorState,
override val isRoomEncrypted: Boolean?,
) : TextEditorState
fun messageHtml(): String? = when (this) {

View File

@@ -75,6 +75,7 @@ class KonsistPreviewTest {
"MatrixBadgeAtomPositivePreview",
"MatrixBadgeAtomNeutralPreview",
"MatrixBadgeAtomNegativePreview",
"MatrixBadgeAtomInfoPreview",
"MentionSpanPreview",
"MessageComposerViewVoicePreview",
"MessagesReactionButtonAddPreview",
@@ -106,14 +107,19 @@ class KonsistPreviewTest {
"TextComposerAddCaptionPreview",
"TextComposerCaptionPreview",
"TextComposerEditPreview",
"TextComposerEditNotEncryptedPreview",
"TextComposerEditCaptionPreview",
"TextComposerFormattingPreview",
"TextComposerFormattingNotEncryptedPreview",
"TextComposerLinkDialogCreateLinkPreview",
"TextComposerLinkDialogCreateLinkWithoutTextPreview",
"TextComposerLinkDialogEditLinkPreview",
"TextComposerReplyPreview",
"TextComposerReplyNotEncryptedPreview",
"TextComposerSimplePreview",
"TextComposerSimpleNotEncryptedPreview",
"TextComposerVoicePreview",
"TextComposerVoiceNotEncryptedPreview",
"TimelineImageWithCaptionRowPreview",
"TimelineItemEventRowForDirectRoomPreview",
"TimelineItemEventRowShieldPreview",

Some files were not shown because too many files have changed in this diff Show More