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:
committed by
GitHub
parent
56683259d9
commit
bb97015e59
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user