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()) { Column(modifier.animateContentSize()) {
if (message.tail == false) { 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) { if (authorIsBlocked) {

View File

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

View File

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

View File

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

View File

@ -1,20 +1,25 @@
package chat.stoat.screens.main package chat.stoat.screens.main
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box 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.WindowInsets
import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
@ -26,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
@ -78,52 +84,95 @@ fun ConversationsScreen(navController: NavController) {
} }
if (notesChannel != null) { if (notesChannel != null) {
ListItem( Row(
headlineContent = { modifier = Modifier
Text(stringResource(R.string.channel_notes)) .fillMaxWidth()
}, .clickable {
supportingContent = { 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()) { if (preview.isNotBlank()) {
Text( Text(
preview, preview,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, 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) { items(100) {
Text( Row(
"Conversation $it", modifier = Modifier modifier = Modifier
.fillMaxWidth()
.clickable { .clickable {
navController.navigate("main/conversation/${it}") 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 package chat.stoat.screens.main
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.stoat.R import chat.stoat.R
import chat.stoat.screens.chat.views.OverviewScreen 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 { enum class MainScreenTab {
Communities, Communities,
@ -28,10 +36,14 @@ enum class MainScreenTab {
@Composable @Composable
fun MainScreen(navController: NavController) { fun MainScreen(navController: NavController) {
var currentTab by rememberSaveable { mutableStateOf(MainScreenTab.Communities) } var currentTab by rememberSaveable { mutableStateOf(MainScreenTab.Communities) }
val hazeState = remember { HazeState() }
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar { NavigationBar(
containerColor = Color.Transparent,
modifier = Modifier.hazeEffect(state = hazeState, style = HazeMaterials.thin(MaterialTheme.colorScheme.surfaceContainer))
) {
NavigationBarItem( NavigationBarItem(
selected = currentTab == MainScreenTab.Communities, selected = currentTab == MainScreenTab.Communities,
onClick = { currentTab = MainScreenTab.Communities }, onClick = { currentTab = MainScreenTab.Communities },
@ -88,22 +100,28 @@ fun MainScreen(navController: NavController) {
} }
}, },
) { pv -> ) { pv ->
Box(Modifier.padding(pv)) { Box(
when (currentTab) { Modifier
MainScreenTab.Communities -> {} .fillMaxSize()
MainScreenTab.Conversations -> { .hazeSource(state = hazeState)
ConversationsScreen( ) {
navController Box(Modifier.padding(pv)) {
) when (currentTab) {
} MainScreenTab.Communities -> {}
MainScreenTab.Conversations -> {
ConversationsScreen(
navController
)
}
MainScreenTab.Overview -> { MainScreenTab.Overview -> {
OverviewScreen( OverviewScreen(
navController, navController,
useDrawer = false, useDrawer = false,
onDrawerClicked = {}, onDrawerClicked = {},
includePadding = false includePadding = false
) )
}
} }
} }
} }

View File

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

View File

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