diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/InviteFriendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/InviteFriendFragment.kt index 2a9aedbd42..e592cfb0fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/InviteFriendFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/InviteFriendFragment.kt @@ -118,7 +118,6 @@ private fun InviteFriend( SlimOutlineCopyButton( modifier = Modifier.weight(1f), - color = LocalColors.current.text, onClick = copyPublicKey ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index 6b45a5680b..f36392ebee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -90,6 +90,8 @@ interface Colors { val qrCodeBackground: Color } +val Colors.slimOutlineButton: Color get() = text + fun sessionColors( isLight: Boolean, isClassic: Boolean, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Strings.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Strings.kt index 14b68c7b80..91500da7b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Strings.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Strings.kt @@ -40,7 +40,6 @@ sealed class GetString { data class FromMap(val value: T, val function: (Context, T) -> String): GetString() { @Composable override fun string(): String = function(LocalContext.current, value) - override fun string(context: Context): String = function(context, value) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt index e2ba7addb3..ef20c882fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Button.kt @@ -2,28 +2,46 @@ package org.thoughtcrime.securesms.ui.components import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonColors import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ButtonElevation +import androidx.compose.material.MaterialTheme import androidx.compose.material.RadioButton import androidx.compose.material.Text import androidx.compose.material.TextButton 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter +import network.loki.messenger.R import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.LaunchedEffectAsync import org.thoughtcrime.securesms.ui.LocalColors import org.thoughtcrime.securesms.ui.baseBold import org.thoughtcrime.securesms.ui.contentDescription @@ -31,7 +49,185 @@ import org.thoughtcrime.securesms.ui.extraSmall import org.thoughtcrime.securesms.ui.h8 import org.thoughtcrime.securesms.ui.h9 import org.thoughtcrime.securesms.ui.radioButtonColors +import org.thoughtcrime.securesms.ui.slimOutlineButton import org.thoughtcrime.securesms.ui.small +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +interface ButtonType { + @Composable fun border(color: Color, enabled: Boolean): BorderStroke? + @Composable fun buttonColors(color: Color): ButtonColors + val elevation: ButtonElevation? @Composable get + + object Outline: ButtonType { + @Composable override fun border(color: Color, enabled: Boolean) = BorderStroke(1.dp, if (enabled) color else LocalColors.current.disabled) + @Composable override fun buttonColors(color: Color) = ButtonDefaults.buttonColors( + contentColor = color, + backgroundColor = Color.Unspecified, + disabledContentColor = LocalColors.current.disabled, + disabledBackgroundColor = Color.Unspecified + ) + override val elevation: ButtonElevation? @Composable get() = null + } + + object Fill: ButtonType { + @Composable override fun border(color: Color, enabled: Boolean) = null + @Composable override fun buttonColors(color: Color) = ButtonDefaults.buttonColors( + contentColor = LocalColors.current.background, + backgroundColor = color, + disabledContentColor = LocalColors.current.disabled, + disabledBackgroundColor = Color.Unspecified + ) + override val elevation: ButtonElevation @Composable get() = ButtonDefaults.elevation() + } +} + +/** + * Base [Button] implementation + */ +@Composable +fun Button( + onClick: () -> Unit, + color: Color, + type: ButtonType, + modifier: Modifier = Modifier, + enabled: Boolean = true, + size: ButtonSize = ButtonSize.Large, + elevation: ButtonElevation? = type.elevation, + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = type.border(color, enabled), + colors: ButtonColors = type.buttonColors(color), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable RowScope.() -> Unit +) { + size.applyConstraints { + androidx.compose.material.Button( + onClick, + modifier.heightIn(min = size.minHeight), + enabled, + interactionSource, + elevation, + shape, + border, + colors, + content = content + ) + } +} + +/** + * Courtesy [Button] implementation for buttons that just display text. + */ +@Composable +fun Button( + text: String, + onClick: () -> Unit, + color: Color, + type: ButtonType, + modifier: Modifier = Modifier, + enabled: Boolean = true, + size: ButtonSize = ButtonSize.Large, + elevation: ButtonElevation? = type.elevation, + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = type.border(color, enabled), + colors: ButtonColors = type.buttonColors(color), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + Button(onClick, color, type, modifier, enabled, size, elevation, shape, border, colors, interactionSource) { + Text(text) + } +} + +@Composable fun FillButton(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { + Button(text, onClick, LocalColors.current.buttonOutline, ButtonType.Fill, modifier, enabled) +} + +@Composable fun PrimaryFillButton(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { + Button(text, onClick, LocalColors.current.primary, ButtonType.Fill, modifier, enabled) +} + +@Composable fun OutlineButton(text: String, modifier: Modifier = Modifier, color: Color = LocalColors.current.buttonOutline, enabled: Boolean = true, onClick: () -> Unit) { + Button(text, onClick, color, ButtonType.Outline, modifier, enabled) +} + +@Composable fun PrimaryOutlineButton(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { + Button(text, onClick, LocalColors.current.primary, ButtonType.Outline, modifier, enabled) +} + +@Composable fun SlimOutlineButton(onClick: () -> Unit, modifier: Modifier = Modifier, color: Color = LocalColors.current.slimOutlineButton, enabled: Boolean = true, content: @Composable () -> Unit) { + Button(onClick, color, ButtonType.Outline, modifier, enabled, ButtonSize.Slim) { content() } +} + +/** + * Courtesy [SlimOutlineButton] implementation for buttons that just display text. + */ +@Composable fun SlimOutlineButton(text: String, modifier: Modifier = Modifier, color: Color = LocalColors.current.slimOutlineButton, enabled: Boolean = true, onClick: () -> Unit) { + Button(text, onClick, color, ButtonType.Outline, modifier, enabled, ButtonSize.Slim) +} + +@Composable +fun SlimOutlineCopyButton( + modifier: Modifier = Modifier, + color: Color = LocalColors.current.slimOutlineButton, + onClick: () -> Unit +) { + OutlineCopyButton(modifier, ButtonSize.Slim, color, onClick) +} + +@Composable +fun OutlineCopyButton( + modifier: Modifier = Modifier, + size: ButtonSize = ButtonSize.Large, + color: Color = LocalColors.current.buttonOutline, + onClick: () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + + Button( + modifier = modifier.contentDescription(R.string.AccessibilityId_copy_button), + interactionSource = interactionSource, + size = size, + type = ButtonType.Outline, + color = color, + onClick = onClick + ) { + TemporaryClickedContent( + interactionSource = interactionSource, + content = { Text(stringResource(R.string.copy)) }, + temporaryContent = { Text(stringResource(R.string.copied)) } + ) + } +} + +@Composable +fun TemporaryClickedContent( + interactionSource: MutableInteractionSource, + content: @Composable () -> Unit, + temporaryContent: @Composable () -> Unit, + temporaryDelay: Duration = 2.seconds +) { + var clicked by remember { mutableStateOf(false) } + + LaunchedEffectAsync { + interactionSource.releases.collectLatest { + clicked = true + delay(temporaryDelay) + clicked = false + } + } + + // Using a Box because the Buttons add children in a Row + // and they will jank as they are added and removed. + Box(contentAlignment = Alignment.Center) { + AnimatedVisibility(!clicked, enter = fadeIn(), exit = fadeOut()) { + content() + } + AnimatedVisibility(clicked, enter = fadeIn(), exit = fadeOut()) { + temporaryContent() + } + } +} + @Composable fun BorderlessButton( diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonSize.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonSize.kt index deb51e0898..25335593f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonSize.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ButtonSize.kt @@ -6,7 +6,6 @@ import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -27,14 +26,15 @@ interface ButtonSize { val textStyle: TextStyle @Composable get val minHeight: Dp -} -object LargeButtonSize: ButtonSize { - override val textStyle @Composable get() = baseBold - override val minHeight = 41.dp -} + object Large: ButtonSize { + override val textStyle @Composable get() = baseBold + override val minHeight = 41.dp + } + + object Slim: ButtonSize { + override val textStyle @Composable get() = extraSmallBold + override val minHeight = 29.dp + } -object SlimButtonSize: ButtonSize { - override val textStyle @Composable get() = extraSmallBold - override val minHeight = 29.dp } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionOutlinedButton.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionOutlinedButton.kt index e0241bfa0d..3da4ae0ec9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionOutlinedButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionOutlinedButton.kt @@ -33,172 +33,3 @@ import org.thoughtcrime.securesms.ui.contentDescription import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -interface ButtonType { - @Composable fun border(color: Color, enabled: Boolean): BorderStroke? - @Composable fun buttonColors(color: Color): ButtonColors - val elevation: ButtonElevation? @Composable get - - object Outline: ButtonType { - @Composable override fun border(color: Color, enabled: Boolean) = BorderStroke(1.dp, if (enabled) color else LocalColors.current.disabled) - @Composable override fun buttonColors(color: Color) = ButtonDefaults.buttonColors( - contentColor = color, - backgroundColor = Color.Unspecified, - disabledContentColor = LocalColors.current.disabled, - disabledBackgroundColor = Color.Unspecified - ) - override val elevation: ButtonElevation? @Composable get() = null - } - object Fill: ButtonType { - @Composable override fun border(color: Color, enabled: Boolean) = null - @Composable override fun buttonColors(color: Color) = ButtonDefaults.buttonColors( - contentColor = LocalColors.current.background, - backgroundColor = color, - disabledContentColor = LocalColors.current.disabled, - disabledBackgroundColor = Color.Unspecified - ) - override val elevation: ButtonElevation? @Composable get() = ButtonDefaults.elevation() - } -} - -/** - * Base [Button] implementation - */ -@Composable -fun Button( - onClick: () -> Unit, - color: Color, - type: ButtonType, - modifier: Modifier = Modifier, - enabled: Boolean = true, - size: ButtonSize = LargeButtonSize, - elevation: ButtonElevation? = type.elevation, - shape: Shape = MaterialTheme.shapes.small, - border: BorderStroke? = type.border(color, enabled), - colors: ButtonColors = type.buttonColors(color), - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - content: @Composable RowScope.() -> Unit -) { - size.applyConstraints { - androidx.compose.material.Button( - onClick, - modifier.heightIn(min = size.minHeight), - enabled, - interactionSource, - elevation, - shape, - border, - colors, - content = content - ) - } -} - -/** - * Courtesy [Button] implementation - */ -@Composable -fun Button( - text: String, - onClick: () -> Unit, - color: Color, - type: ButtonType, - modifier: Modifier = Modifier, - enabled: Boolean = true, - size: ButtonSize = LargeButtonSize, - elevation: ButtonElevation? = type.elevation, - shape: Shape = MaterialTheme.shapes.small, - border: BorderStroke? = type.border(color, enabled), - colors: ButtonColors = type.buttonColors(color), - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, -) { - Button(onClick, color, type, modifier, enabled, size, elevation, shape, border, colors, interactionSource) { - Text(text) - } -} - -@Composable fun FillButton(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { - Button(text, onClick, LocalColors.current.buttonOutline, ButtonType.Fill, modifier, enabled) -} - -@Composable fun PrimaryFillButton(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { - Button(text, onClick, LocalColors.current.primary, ButtonType.Fill, modifier, enabled) -} - -@Composable fun OutlineButton(text: String, modifier: Modifier = Modifier, color: Color = LocalColors.current.buttonOutline, enabled: Boolean = true, onClick: () -> Unit) { - Button(text, onClick, color, ButtonType.Outline, modifier, enabled) -} - -@Composable fun PrimaryOutlineButton(text: String, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { - Button(text, onClick, LocalColors.current.primary, ButtonType.Outline, modifier, enabled) -} - -@Composable fun SlimOutlineButton(text: String, modifier: Modifier = Modifier, color: Color = LocalColors.current.buttonOutline, enabled: Boolean = true, onClick: () -> Unit) { - Button(text, onClick, color, ButtonType.Outline, modifier, enabled, SlimButtonSize) -} - -@Composable fun SlimOutlineButton(onClick: () -> Unit, modifier: Modifier = Modifier, color: Color = LocalColors.current.buttonOutline, enabled: Boolean = true, content: @Composable () -> Unit) { - Button(onClick, color, ButtonType.Outline, modifier, enabled, SlimButtonSize) { content() } -} - -@Composable -fun SlimOutlineCopyButton( - modifier: Modifier = Modifier, - color: Color = LocalColors.current.buttonOutline, - onClick: () -> Unit -) { - OutlineCopyButton(modifier, SlimButtonSize, color, onClick) -} - -@Composable -fun OutlineCopyButton( - modifier: Modifier = Modifier, - size: ButtonSize = LargeButtonSize, - color: Color = LocalColors.current.buttonOutline, - onClick: () -> Unit -) { - val interactionSource = remember { MutableInteractionSource() } - - Button( - modifier = modifier.contentDescription(R.string.AccessibilityId_copy_button), - interactionSource = interactionSource, - size = size, - type = ButtonType.Outline, - color = color, - onClick = onClick - ) { - TemporaryClickedContent( - interactionSource = interactionSource, - content = { Text(stringResource(R.string.copy)) }, - temporaryContent = { Text(stringResource(R.string.copied)) } - ) - } -} - -@Composable -fun TemporaryClickedContent( - interactionSource: MutableInteractionSource, - content: @Composable () -> Unit, - temporaryContent: @Composable () -> Unit, - temporaryDelay: Duration = 2.seconds -) { - var clicked by remember { mutableStateOf(false) } - - LaunchedEffectAsync { - interactionSource.releases.collectLatest { - clicked = true - delay(temporaryDelay) - clicked = false - } - } - - // Using a Box because the Buttons add children in a Row - // and they will jank as they are added and removed. - Box(contentAlignment = Alignment.Center) { - AnimatedVisibility(!clicked, enter = fadeIn(), exit = fadeOut()) { - content() - } - AnimatedVisibility(clicked, enter = fadeIn(), exit = fadeOut()) { - temporaryContent() - } - } -}