feat: redesign app with modern Material UI and glassmorphism

Redesigned the application's UI to align with modern Material 3
principles, featuring:
- Integration of the 'haze' library for glassmorphism (frosted glass)
  effects on TopAppBars and NavigationBars.
- Updated theme logic to prioritize dynamic color schemes on
  supported devices.
- Refined message layout in ChannelScreen with improved spacing
  for tail messages.
- Redesigned message input field in ChannelScreen as a modern,
  rounded pill.
- Modernized list items and removed dividers in ConversationsScreen
  for a cleaner, more minimal look.
- Polished card designs and spacing in OverviewScreen.
- Verified build integrity and fixed several layout and compilation
  issues.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
neuralshyam 2026-04-04 21:17:22 +00:00
parent 4ba9b59127
commit eecbceabb8
8 changed files with 147 additions and 67 deletions

View File

@ -254,7 +254,9 @@ fun Message(
Column(modifier.animateContentSize()) {
if (message.tail == false) {
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = Modifier.height(12.dp))
} else {
Spacer(modifier = Modifier.height(2.dp))
}
if (authorIsBlocked) {

View File

@ -54,6 +54,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
@ -260,7 +261,7 @@ fun MessageField(
}
Column(
modifier = modifier.background(MaterialTheme.colorScheme.surfaceContainer)
modifier = modifier.background(Color.Transparent)
) {
AnimatedVisibility(
visible = autocompleteSuggestions.isNotEmpty(),
@ -491,7 +492,9 @@ fun MessageField(
}
Row(
modifier = modifier
.background(MaterialTheme.colorScheme.surfaceContainer),
.padding(horizontal = 8.dp, vertical = 4.dp)
.clip(MaterialTheme.shapes.extraLarge)
.background(MaterialTheme.colorScheme.surfaceContainerHigh),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(8.dp))

View File

@ -413,8 +413,8 @@ fun OverviewScreenLink(
modifier = Modifier
.clip(MaterialTheme.shapes.extraLarge)
.then(if (clickable) Modifier.clickable(onClick = onClick) else Modifier)
.background(backgroundColour)
.padding(vertical = 32.dp)
.background(backgroundColour.copy(alpha = 0.8f))
.padding(vertical = 24.dp)
.fillMaxWidth()
) {
CompositionLocalProvider(LocalContentColor provides foregroundColour) {

View File

@ -86,6 +86,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@ -147,6 +148,10 @@ import chat.stoat.screens.chat.LocalIsConnected
import chat.stoat.sheets.ChannelInfoSheet
import chat.stoat.sheets.MessageContextSheet
import chat.stoat.sheets.ReactSheet
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.HazeMaterials
import com.valentinilk.shimmer.ShimmerBounds
import com.valentinilk.shimmer.rememberShimmer
import com.valentinilk.shimmer.shimmer
@ -203,6 +208,7 @@ fun ChannelScreen(
// <editor-fold desc="State and effects">
val scope = rememberCoroutineScope()
val context = LocalContext.current
val hazeState = remember { HazeState() }
val config = LocalConfiguration.current
LaunchedEffect(Unit) {
@ -524,7 +530,8 @@ fun ChannelScreen(
TopAppBar(
modifier = Modifier.clickable {
channelInfoSheetShown = true
},
}.hazeEffect(state = hazeState, style = HazeMaterials.thin(MaterialTheme.colorScheme.surface)),
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -675,7 +682,7 @@ fun ChannelScreen(
.padding(pv)
) {
Box(
modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f).hazeSource(state = hazeState),
contentAlignment = Alignment.BottomCenter
) {
LazyColumn(
@ -963,7 +970,8 @@ fun ChannelScreen(
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceContainer)
.hazeEffect(state = hazeState, style = HazeMaterials.thin(MaterialTheme.colorScheme.surfaceContainer))
.background(Color.Transparent)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally

View File

@ -1,20 +1,25 @@
package chat.stoat.screens.main
import androidx.compose.foundation.background
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.WindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Badge
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
@ -26,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
@ -78,52 +84,95 @@ fun ConversationsScreen(navController: NavController) {
}
if (notesChannel != null) {
ListItem(
headlineContent = {
Text(stringResource(R.string.channel_notes))
},
supportingContent = {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate("main/conversation/${notesChannel.id}")
}
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(contentAlignment = Alignment.TopEnd) {
StoatAPI.userCache[StoatAPI.selfId]?.let {
UserAvatar(
username = it.username.toString(),
avatar = it.avatar,
userId = it.id.toString(),
shape = RoundedCornerShape(LoadedSettings.avatarRadius)
)
}
Badge {
Icon(
painter = painterResource(R.drawable.ic_keep_24dp),
contentDescription = null,
modifier = Modifier.size(12.dp)
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
stringResource(R.string.channel_notes),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
if (preview.isNotBlank()) {
Text(
preview,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
leadingContent = {
Box(contentAlignment = Alignment.TopEnd) {
StoatAPI.userCache[StoatAPI.selfId]?.let {
UserAvatar(
username = it.username.toString(),
avatar = it.avatar,
userId = it.id.toString(),
shape = RoundedCornerShape(LoadedSettings.avatarRadius)
)
}
Badge {
Icon(
painter = painterResource(R.drawable.ic_keep_24dp),
contentDescription = null,
modifier = Modifier.size(12.dp)
)
}
}
},
modifier = Modifier.clickable {
navController.navigate("main/conversation/${notesChannel.id}")
}
)
HorizontalDivider()
}
}
}
items(1000) {
Text(
"Conversation $it", modifier = Modifier
items(100) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate("main/conversation/${it}")
}
.fillMaxWidth())
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(LoadedSettings.avatarRadius.dp * 2)
.clip(RoundedCornerShape(LoadedSettings.avatarRadius))
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Text(
text = it.toString().take(1),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
"Conversation $it",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
"Last message preview $it",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}

View File

@ -1,23 +1,31 @@
package chat.stoat.screens.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import chat.stoat.R
import chat.stoat.screens.chat.views.OverviewScreen
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.HazeMaterials
enum class MainScreenTab {
Communities,
@ -28,10 +36,14 @@ enum class MainScreenTab {
@Composable
fun MainScreen(navController: NavController) {
var currentTab by rememberSaveable { mutableStateOf(MainScreenTab.Communities) }
val hazeState = remember { HazeState() }
Scaffold(
bottomBar = {
NavigationBar {
NavigationBar(
containerColor = Color.Transparent,
modifier = Modifier.hazeEffect(state = hazeState, style = HazeMaterials.thin(MaterialTheme.colorScheme.surfaceContainer))
) {
NavigationBarItem(
selected = currentTab == MainScreenTab.Communities,
onClick = { currentTab = MainScreenTab.Communities },
@ -88,22 +100,28 @@ fun MainScreen(navController: NavController) {
}
},
) { pv ->
Box(Modifier.padding(pv)) {
when (currentTab) {
MainScreenTab.Communities -> {}
MainScreenTab.Conversations -> {
ConversationsScreen(
navController
)
}
Box(
Modifier
.fillMaxSize()
.hazeSource(state = hazeState)
) {
Box(Modifier.padding(pv)) {
when (currentTab) {
MainScreenTab.Communities -> {}
MainScreenTab.Conversations -> {
ConversationsScreen(
navController
)
}
MainScreenTab.Overview -> {
OverviewScreen(
navController,
useDrawer = false,
onDrawerClicked = {},
includePadding = false
)
MainScreenTab.Overview -> {
OverviewScreen(
navController,
useDrawer = false,
onDrawerClicked = {},
includePadding = false
)
}
}
}
}

View File

@ -3,9 +3,9 @@ package chat.stoat.ui.theme
import androidx.compose.ui.graphics.Color
object Colour {
val RevoltUltraPink = Color(0xFFFF005C)
val RevoltUltraPink = Color(0xFFFF3377)
val PrimaryLight = Color(0xFFBD0042)
val PrimaryLight = Color(0xFFB90041)
val OnPrimaryLight = Color(0xFFFFFFFF)
val PrimaryContainerLight = Color(0xFFFFD9DC)
val OnPrimaryContainerLight = Color(0xFF910031)
@ -41,10 +41,10 @@ object Colour {
val SurfaceContainerHighLight = Color(0xFFE4E8F8)
val SurfaceContainerHighestLight = Color(0xFFDEE2F2)
val PrimaryDark = Color(0xFFFFB2BA)
val OnPrimaryDark = Color(0xFF670020)
val PrimaryContainerDark = Color(0xFF910031)
val OnPrimaryContainerDark = Color(0xFFFFD9DC)
val PrimaryDark = Color(0xFFFFB2BB)
val OnPrimaryDark = Color(0xFF67001F)
val PrimaryContainerDark = Color(0xFF910030)
val OnPrimaryContainerDark = Color(0xFFFFD9DD)
val SecondaryDark = Color(0xFFFFB3AE)
val OnSecondaryDark = Color(0xFF68000C)
val SecondaryContainerDark = Color(0xFF930015)

View File

@ -129,11 +129,11 @@ fun getColorScheme(
val m3Supported = systemSupportsDynamicColors()
val colorScheme = when {
m3Supported && requestedTheme == Theme.M3Dynamic && systemInDarkTheme -> dynamicDarkColorScheme(
m3Supported && (requestedTheme == Theme.M3Dynamic || requestedTheme == Theme.None) && systemInDarkTheme -> dynamicDarkColorScheme(
context
)
m3Supported && requestedTheme == Theme.M3Dynamic && !systemInDarkTheme -> dynamicLightColorScheme(
m3Supported && (requestedTheme == Theme.M3Dynamic || requestedTheme == Theme.None) && !systemInDarkTheme -> dynamicLightColorScheme(
context
)