ui: improve Compose stability and message UI

- Add minute/second abbreviations and kotlin.time-based relative time formatter
- Introduce FastPreview and update previews to use AppTheme with dark/dynamic colors
- Refactor attachments preview grid & waveform to use ImmutableList and reduce recompositions
- Tweak message bubble reply styling and swipe-to-reply animation/haptics
- Add Compose Stability Analyzer plugin and enable it in debug builds
- Cache shared images by sha256 and improve share intent/chooser text
- Minor UX polish (e.g., “No views”) and immutability annotations
This commit is contained in:
2026-02-06 22:14:01 +03:00
parent e3e9157dd5
commit 96b4fc8539
30 changed files with 341 additions and 187 deletions
@@ -0,0 +1,48 @@
package dev.meloda.fast.ui.common
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_TYPE_NORMAL
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.Wallpapers.BLUE_DOMINATED_EXAMPLE
import androidx.compose.ui.tooling.preview.Wallpapers.GREEN_DOMINATED_EXAMPLE
import androidx.compose.ui.tooling.preview.Wallpapers.RED_DOMINATED_EXAMPLE
import androidx.compose.ui.tooling.preview.Wallpapers.YELLOW_DOMINATED_EXAMPLE
@Preview(name = "70%", fontScale = 0.70f)
@Preview(name = "85%", fontScale = 0.85f)
@Preview(name = "100%", fontScale = 1.0f)
@Preview(name = "115%", fontScale = 1.15f)
@Preview(name = "130%", fontScale = 1.3f)
@Preview(name = "150%", fontScale = 1.5f)
@Preview(name = "180%", fontScale = 1.8f)
@Preview(name = "200%", fontScale = 2f)
@Preview(name = "Light")
@Preview(name = "Red", wallpaper = RED_DOMINATED_EXAMPLE)
@Preview(name = "Blue", wallpaper = BLUE_DOMINATED_EXAMPLE)
@Preview(name = "Green", wallpaper = GREEN_DOMINATED_EXAMPLE)
@Preview(name = "Yellow", wallpaper = YELLOW_DOMINATED_EXAMPLE)
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL)
@Preview(
name = "Dark Red",
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
wallpaper = RED_DOMINATED_EXAMPLE
)
@Preview(
name = "Dark Blue",
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
wallpaper = BLUE_DOMINATED_EXAMPLE
)
@Preview(
name = "Dark Green",
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
wallpaper = GREEN_DOMINATED_EXAMPLE
)
@Preview(
name = "Dark Yellow",
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
wallpaper = YELLOW_DOMINATED_EXAMPLE
)
annotation class FastPreview
@@ -2,6 +2,7 @@ package dev.meloda.fast.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -46,9 +47,14 @@ import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
import androidx.compose.ui.tooling.preview.PreviewFontScale
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.FastPreview
import dev.meloda.fast.ui.theme.AppTheme
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
@@ -361,10 +367,10 @@ sealed class SelectionType {
data object None : SelectionType()
}
@Preview
@FastPreview
@Composable
private fun MaterialDialogPreview() {
AppTheme {
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
MaterialDialog(
onDismissRequest = {},
title = "Material Dialog",
@@ -376,10 +382,10 @@ private fun MaterialDialogPreview() {
}
}
@Preview
@FastPreview
@Composable
private fun MaterialDialogWithListPreview() {
AppTheme {
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
MaterialDialog(
onDismissRequest = {},
title = "Material Dialog",
@@ -393,10 +399,10 @@ private fun MaterialDialogWithListPreview() {
}
}
@Preview
@FastPreview
@Composable
private fun MaterialDialogWithCustomContent() {
AppTheme {
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
MaterialDialog(
onDismissRequest = {},
title = "Material Dialog",
@@ -425,10 +431,10 @@ private fun MaterialDialogWithCustomContent() {
}
}
@Preview
@FastPreview
@Composable
private fun MaterialDialogWithOnlyCustomContent() {
AppTheme {
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
MaterialDialog(onDismissRequest = {}) {
Row(
modifier = Modifier
@@ -1,5 +1,6 @@
package dev.meloda.fast.ui.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -8,15 +9,17 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.common.FastPreview
import dev.meloda.fast.ui.theme.AppTheme
@Composable
fun NoItemsView(
@@ -49,11 +52,15 @@ fun NoItemsView(
}
}
@Preview
@FastPreview
@Composable
private fun NoItemsViewPreview() {
NoItemsView(
customText = "Nothing here...",
buttonText = "Refresh"
)
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
Surface {
NoItemsView(
customText = "Nothing here...",
buttonText = "Refresh"
)
}
}
}
@@ -9,6 +9,7 @@ import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
@Suppress("ParamsComparedByRef")
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
navController: NavController,
@@ -9,10 +9,6 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
operator fun get(index: Int): T = values[index]
inline fun forEach(action: (T) -> Unit) {
for (element in values) action(element)
}
inline fun <R> map(transform: (T) -> R): ImmutableList<R> {
return values.map(transform).toImmutableList()
}
@@ -49,6 +45,8 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
if (elements.isNotEmpty()) copyOf(elements.asList()) else empty()
fun <T> of(element: T) = ImmutableList(listOf(element))
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
}
override fun iterator(): Iterator<T> = values.listIterator()
@@ -59,5 +57,3 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
fun <T> emptyImmutableList(): ImmutableList<T> = ImmutableList(emptyList())
fun <T> immutableListOf(vararg elements: T) = ImmutableList(listOf(elements = elements))
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
@@ -232,8 +232,10 @@
<string name="month_short">М</string>
<string name="week_short">Н</string>
<string name="day_short">Д</string>
<string name="second_short">С</string>
<string name="time_now">Сейчас</string>
<string name="confirm_chat_create_with_title">Вы действительно хотите создать чат «%s»?</string>
<string name="confirm_chat_create_empty_with_title">Вы действительно хотите создать чат «%s» только с собой?</string>
<string name="minute_short">М</string>
</resources>
+3 -1
View File
@@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<string name="app_name" translatable="false">Fast</string>
<string name="fast_messenger" translatable="false">Fast Messenger</string>
@@ -297,6 +297,8 @@
<string name="month_short">M</string>
<string name="week_short">W</string>
<string name="day_short">D</string>
<string name="minute_short">M</string>
<string name="second_short">S</string>
<string name="time_now">Now</string>
<string name="confirm_chat_create_with_title">Are you sure you want to create chat «%s»?</string>