feat: m3 list style

This commit is contained in:
infi 2026-06-17 20:16:29 +02:00
parent 8c197165b3
commit 3a07d78da3
7 changed files with 301 additions and 191 deletions

View File

@ -1,22 +1,28 @@
package chat.stoat.composables.chat package chat.stoat.composables.chat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import chat.stoat.api.internals.BrushCompat import chat.stoat.api.internals.BrushCompat
import chat.stoat.api.internals.Roles import chat.stoat.api.internals.Roles
import chat.stoat.api.internals.solidColor import chat.stoat.api.internals.solidColor
import chat.stoat.core.model.schemas.Member
import chat.stoat.core.model.schemas.User
import chat.stoat.composables.generic.UserAvatar import chat.stoat.composables.generic.UserAvatar
import chat.stoat.composables.generic.presenceFromStatus import chat.stoat.composables.generic.presenceFromStatus
import chat.stoat.core.model.data.STOAT_FILES import chat.stoat.core.model.data.STOAT_FILES
import chat.stoat.internals.extensions.TransparentListItemColours import chat.stoat.core.model.schemas.Member
import chat.stoat.core.model.schemas.User
private val NoneLambda = { }
@Composable @Composable
fun MemberListItem( fun MemberListItem(
@ -25,6 +31,10 @@ fun MemberListItem(
serverId: String?, serverId: String?,
userId: String, userId: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
first: Boolean = false,
last: Boolean = false,
onClick: () -> Unit = {},
onLongClick: () -> Unit = NoneLambda,
trailingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null,
) { ) {
val highestColourRole = serverId?.let { val highestColourRole = serverId?.let {
@ -41,8 +51,43 @@ fun MemberListItem(
?: Brush.solidColor(LocalContentColor.current) ?: Brush.solidColor(LocalContentColor.current)
ListItem( ListItem(
colors = TransparentListItemColours, colors = ListItemDefaults.colors(
modifier = modifier, containerColor = MaterialTheme.colorScheme.surfaceContainer
),
modifier = modifier
.then(
when {
first && last -> Modifier.clip(MaterialTheme.shapes.large)
first -> Modifier.clip(
MaterialTheme.shapes.extraSmall.copy(
topStart = MaterialTheme.shapes.large.topStart,
topEnd = MaterialTheme.shapes.large.topEnd
)
)
last -> Modifier.clip(
MaterialTheme.shapes.extraSmall.copy(
bottomStart = MaterialTheme.shapes.large.bottomStart,
bottomEnd = MaterialTheme.shapes.large.bottomEnd
)
)
else -> Modifier.clip(MaterialTheme.shapes.extraSmall)
}
)
.then(
if (onLongClick != NoneLambda) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
} else {
Modifier.clickable(
onClick = onClick
)
}
),
headlineContent = { headlineContent = {
Text( Text(
text = member?.nickname text = member?.nickname

View File

@ -7,6 +7,6 @@ import androidx.compose.ui.graphics.Color
val TransparentListItemColours: ListItemColors val TransparentListItemColours: ListItemColors
@Composable @Composable
get() = ListItemDefaults.colors().copy( get() = ListItemDefaults.colors(
containerColor = Color.Transparent containerColor = Color.Transparent
) )

View File

@ -7,7 +7,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -179,8 +178,24 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
text = AnnotatedString.fromHtml( text = AnnotatedString.fromHtml(
stringResource( stringResource(
R.string.friends_add_by_tag_sheet_description, R.string.friends_add_by_tag_sheet_description,
"<font color=\"${Color(HL_USERNAME).toArgb().let { "#%02x%02x%02x".format(it shr 16 and 0xff, it shr 8 and 0xff, it and 0xff) }}\">", "<font color=\"${
"<font color=\"${Color(HL_TAG).toArgb().let { "#%02x%02x%02x".format(it shr 16 and 0xff, it shr 8 and 0xff, it and 0xff) }}\">", Color(HL_USERNAME).toArgb().let {
"#%02x%02x%02x".format(
it shr 16 and 0xff,
it shr 8 and 0xff,
it and 0xff
)
}
}\">",
"<font color=\"${
Color(HL_TAG).toArgb().let {
"#%02x%02x%02x".format(
it shr 16 and 0xff,
it shr 8 and 0xff,
it and 0xff
)
}
}\">",
"</font>", "</font>",
) )
), ),
@ -601,19 +616,28 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
val item = FriendRequests.getIncoming().getOrNull(it) val item = FriendRequests.getIncoming().getOrNull(it)
if (item == null) return@items if (item == null) return@items
val isLast = it == FriendRequests.getIncoming().size - 1
MemberListItem( MemberListItem(
member = null, member = null,
user = item, user = item,
serverId = null, serverId = null,
userId = item.id ?: "", userId = item.id ?: "",
modifier = Modifier.clickable { first = it == 0,
last = isLast,
onClick = {
scope.launch { scope.launch {
item.id?.let { userId -> item.id?.let { userId ->
ActionChannel.send(Action.OpenUserSheet(userId, null)) ActionChannel.send(Action.OpenUserSheet(userId, null))
} }
} }
} },
modifier = Modifier.padding(horizontal = 16.dp)
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
stickyHeader(key = "outgoing") { stickyHeader(key = "outgoing") {
@ -627,19 +651,28 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
val item = FriendRequests.getOutgoing().getOrNull(it) val item = FriendRequests.getOutgoing().getOrNull(it)
if (item == null) return@items if (item == null) return@items
val isLast = it == FriendRequests.getOutgoing().size - 1
MemberListItem( MemberListItem(
member = null, member = null,
user = item, user = item,
serverId = null, serverId = null,
userId = item.id ?: "", userId = item.id ?: "",
modifier = Modifier.clickable { first = it == 0,
last = isLast,
onClick = {
scope.launch { scope.launch {
item.id?.let { userId -> item.id?.let { userId ->
ActionChannel.send(Action.OpenUserSheet(userId, null)) ActionChannel.send(Action.OpenUserSheet(userId, null))
} }
} }
} },
modifier = Modifier.padding(horizontal = 16.dp)
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
stickyHeader(key = "online") { stickyHeader(key = "online") {
@ -653,19 +686,28 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
val item = FriendRequests.getOnlineFriends().getOrNull(it) val item = FriendRequests.getOnlineFriends().getOrNull(it)
if (item == null) return@items if (item == null) return@items
val isLast = it == FriendRequests.getOnlineFriends().size - 1
MemberListItem( MemberListItem(
member = null, member = null,
user = item, user = item,
serverId = null, serverId = null,
userId = item.id ?: "", userId = item.id ?: "",
modifier = Modifier.clickable { first = it == 0,
last = isLast,
onClick = {
scope.launch { scope.launch {
item.id?.let { userId -> item.id?.let { userId ->
ActionChannel.send(Action.OpenUserSheet(userId, null)) ActionChannel.send(Action.OpenUserSheet(userId, null))
} }
} }
} },
modifier = Modifier.padding(horizontal = 16.dp)
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
stickyHeader(key = "not_online") { stickyHeader(key = "not_online") {
@ -679,19 +721,28 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
val item = FriendRequests.getFriends(true).getOrNull(it) val item = FriendRequests.getFriends(true).getOrNull(it)
if (item == null) return@items if (item == null) return@items
val isLast = it == FriendRequests.getFriends(true).size - 1
MemberListItem( MemberListItem(
member = null, member = null,
user = item, user = item,
serverId = null, serverId = null,
userId = item.id ?: "", userId = item.id ?: "",
modifier = Modifier.clickable { first = it == 0,
last = isLast,
onClick = {
scope.launch { scope.launch {
item.id?.let { userId -> item.id?.let { userId ->
ActionChannel.send(Action.OpenUserSheet(userId, null)) ActionChannel.send(Action.OpenUserSheet(userId, null))
} }
} }
} },
modifier = Modifier.padding(horizontal = 16.dp)
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
stickyHeader(key = "blocked") { stickyHeader(key = "blocked") {
@ -706,19 +757,28 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
val item = FriendRequests.getBlocked().getOrNull(it) val item = FriendRequests.getBlocked().getOrNull(it)
if (item == null) return@items if (item == null) return@items
val isLast = it == FriendRequests.getBlocked().size - 1
MemberListItem( MemberListItem(
member = null, member = null,
user = item, user = item,
serverId = null, serverId = null,
userId = item.id ?: "", userId = item.id ?: "",
modifier = Modifier.clickable { first = it == 0,
last = isLast,
onClick = {
scope.launch { scope.launch {
item.id?.let { userId -> item.id?.let { userId ->
ActionChannel.send(Action.OpenUserSheet(userId, null)) ActionChannel.send(Action.OpenUserSheet(userId, null))
} }
} }
} },
modifier = Modifier.padding(horizontal = 16.dp)
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
} }

View File

@ -3,10 +3,11 @@ package chat.stoat.screens.create
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -192,13 +193,16 @@ fun CreateGroupScreen(
val friend = StoatAPI.userCache[viewModel.friendsFilteredBySearch[index]] val friend = StoatAPI.userCache[viewModel.friendsFilteredBySearch[index]]
?: return@items ?: return@items
val isMember = viewModel.groupMembers.contains(friend.id) val isMember = viewModel.groupMembers.contains(friend.id)
val isLast = index == viewModel.friendsFilteredBySearch.size - 1
MemberListItem( MemberListItem(
member = null, member = null,
user = friend, user = friend,
serverId = null, serverId = null,
userId = friend.id!!, userId = friend.id!!,
modifier = Modifier.clickable { first = index == 0,
last = isLast,
onClick = {
if (isMember) { if (isMember) {
viewModel.groupMembers.remove(friend.id) viewModel.groupMembers.remove(friend.id)
} else { } else {
@ -207,6 +211,7 @@ fun CreateGroupScreen(
} }
} }
}, },
modifier = Modifier.padding(horizontal = 16.dp),
trailingContent = { trailingContent = {
Checkbox( Checkbox(
checked = isMember, checked = isMember,
@ -215,6 +220,9 @@ fun CreateGroupScreen(
) )
} }
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
} }
} }

View File

@ -4,7 +4,9 @@ import android.content.Intent
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.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -13,6 +15,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@ -21,6 +24,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
@ -100,18 +104,15 @@ fun SettingsScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(vertical = 10.dp) .padding(bottom = 10.dp)
) { ) {
ListHeader { ListHeader {
Text(stringResource(R.string.settings_category_account)) Text(stringResource(R.string.settings_category_account))
} }
ListItem( SettingsListItem(
headlineContent = { first = true,
Text( headlineContent = { Text(text = stringResource(id = R.string.settings_account)) },
text = stringResource(id = R.string.settings_account)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -122,17 +123,11 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_account") .testTag("settings_view_account")
.clickable { .clickable { navController.navigate("settings/account") }
navController.navigate("settings/account")
}
) )
Spacer(Modifier.height(2.dp))
ListItem( SettingsListItem(
headlineContent = { headlineContent = { Text(text = stringResource(id = R.string.settings_profile)) },
Text(
text = stringResource(id = R.string.settings_profile)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -143,17 +138,12 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_profile") .testTag("settings_view_profile")
.clickable { .clickable { navController.navigate("settings/profile") }
navController.navigate("settings/profile")
}
) )
Spacer(Modifier.height(2.dp))
ListItem( SettingsListItem(
headlineContent = { last = true,
Text( headlineContent = { Text(text = stringResource(id = R.string.settings_sessions)) },
text = stringResource(id = R.string.settings_sessions)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -164,21 +154,16 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_sessions") .testTag("settings_view_sessions")
.clickable { .clickable { navController.navigate("settings/sessions") }
navController.navigate("settings/sessions")
}
) )
ListHeader { ListHeader {
Text(stringResource(R.string.settings_category_general)) Text(stringResource(R.string.settings_category_general))
} }
ListItem( SettingsListItem(
headlineContent = { first = true,
Text( headlineContent = { Text(text = stringResource(id = R.string.settings_appearance)) },
text = stringResource(id = R.string.settings_appearance)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -189,17 +174,11 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_appearance") .testTag("settings_view_appearance")
.clickable { .clickable { navController.navigate("settings/appearance") }
navController.navigate("settings/appearance")
}
) )
Spacer(Modifier.height(2.dp))
ListItem( SettingsListItem(
headlineContent = { headlineContent = { Text(text = stringResource(id = R.string.settings_language)) },
Text(
text = stringResource(id = R.string.settings_language)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -210,17 +189,11 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_language") .testTag("settings_view_language")
.clickable { .clickable { navController.navigate("settings/language") }
navController.navigate("settings/language")
}
) )
Spacer(Modifier.height(2.dp))
ListItem( SettingsListItem(
headlineContent = { headlineContent = { Text(text = stringResource(id = R.string.settings_chat)) },
Text(
text = stringResource(id = R.string.settings_chat)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -231,16 +204,12 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_chat") .testTag("settings_view_chat")
.clickable { .clickable { navController.navigate("settings/chat") }
navController.navigate("settings/chat")
}
) )
ListItem( Spacer(Modifier.height(2.dp))
headlineContent = { SettingsListItem(
Text( last = true,
text = stringResource(id = R.string.settings_notifications) headlineContent = { Text(text = stringResource(id = R.string.settings_notifications)) },
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -251,21 +220,25 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_notifications") .testTag("settings_view_notifications")
.clickable { .clickable { navController.navigate("settings/notifications") }
navController.navigate("settings/notifications")
}
) )
ListHeader { ListHeader {
Text(stringResource(R.string.settings_category_miscellaneous)) Text(stringResource(R.string.settings_category_miscellaneous))
} }
ListItem( val miscLastIsExperiments = LoadedSettings.experimentsEnabled
headlineContent = { val miscLastIsLabs =
Text( !miscLastIsExperiments && FeatureFlags.labsAccessControlGranted
text = stringResource(id = R.string.about) val miscLastIsDebug =
) !miscLastIsExperiments && !miscLastIsLabs && BuildConfig.DEBUG
}, val miscLastIsAbout =
!miscLastIsExperiments && !miscLastIsLabs && !miscLastIsDebug
SettingsListItem(
first = true,
last = miscLastIsAbout,
headlineContent = { Text(text = stringResource(id = R.string.about)) },
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -276,18 +249,14 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_about") .testTag("settings_view_about")
.clickable { .clickable { navController.navigate("about") }
navController.navigate("about")
}
) )
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
ListItem( Spacer(Modifier.height(2.dp))
headlineContent = { SettingsListItem(
Text( last = miscLastIsDebug,
text = "Debug" headlineContent = { Text(text = "Debug") },
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -298,19 +267,15 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_debug") .testTag("settings_view_debug")
.clickable { .clickable { navController.navigate("settings/debug") }
navController.navigate("settings/debug")
}
) )
} }
if (FeatureFlags.labsAccessControlGranted) { if (FeatureFlags.labsAccessControlGranted) {
ListItem( Spacer(Modifier.height(2.dp))
headlineContent = { SettingsListItem(
Text( last = miscLastIsLabs,
text = "Labs" headlineContent = { Text(text = "Labs") },
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -321,19 +286,15 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_labs") .testTag("settings_view_labs")
.clickable { .clickable { navController.navigate("labs") }
navController.navigate("labs")
}
) )
} }
if (LoadedSettings.experimentsEnabled) { if (LoadedSettings.experimentsEnabled) {
ListItem( Spacer(Modifier.height(2.dp))
headlineContent = { SettingsListItem(
Text( last = true,
text = "Experiments" headlineContent = { Text(text = "Experiments") },
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -344,9 +305,7 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_experiments") .testTag("settings_view_experiments")
.clickable { .clickable { navController.navigate("settings/experiments") }
navController.navigate("settings/experiments")
}
) )
} }
@ -359,12 +318,9 @@ fun SettingsScreen(
) )
} }
ListItem( SettingsListItem(
headlineContent = { first = true,
Text( headlineContent = { Text(text = stringResource(id = R.string.settings_changelog)) },
text = stringResource(id = R.string.settings_changelog)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -375,22 +331,12 @@ fun SettingsScreen(
}, },
modifier = Modifier modifier = Modifier
.testTag("settings_view_changelog") .testTag("settings_view_changelog")
.clickable { .clickable { navController.navigate("changelog/latest") }
navController.navigate("changelog/latest")
}
) )
Spacer(Modifier.height(2.dp))
ListItem( SettingsListItem(
headlineContent = { headlineContent = { Text(text = stringResource(id = R.string.settings_feedback)) },
Text( supportingContent = { Text(text = stringResource(id = R.string.settings_feedback_description)) },
text = stringResource(id = R.string.settings_feedback)
)
},
supportingContent = {
Text(
text = stringResource(id = R.string.settings_feedback_description)
)
},
leadingContent = { leadingContent = {
SettingsIcon { SettingsIcon {
Icon( Icon(
@ -406,18 +352,16 @@ fun SettingsScreen(
context, context,
InviteActivity::class.java InviteActivity::class.java
).setAction(Intent.ACTION_VIEW) ).setAction(Intent.ACTION_VIEW)
intent.data = "https://stt.gg/Testers".toUri() intent.data = "https://stt.gg/Testers".toUri()
context.startActivity(intent) context.startActivity(intent)
} }
) )
Spacer(Modifier.height(2.dp))
ListItem( SettingsListItem(
last = true,
headlineContent = { headlineContent = {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
Text( Text(text = stringResource(id = R.string.logout))
text = stringResource(id = R.string.logout)
)
} }
}, },
leadingContent = { leadingContent = {
@ -445,6 +389,42 @@ fun SettingsScreen(
} }
} }
@Composable
private fun SettingsListItem(
first: Boolean = false,
last: Boolean = false,
headlineContent: @Composable () -> Unit,
supportingContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable () -> Unit)? = null,
modifier: Modifier = Modifier,
) {
ListItem(
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
headlineContent = headlineContent,
supportingContent = supportingContent,
leadingContent = leadingContent,
modifier = Modifier
.padding(horizontal = 16.dp)
.clip(
when {
first && last -> MaterialTheme.shapes.large
first -> MaterialTheme.shapes.extraSmall.copy(
topStart = MaterialTheme.shapes.large.topStart,
topEnd = MaterialTheme.shapes.large.topEnd
)
last -> MaterialTheme.shapes.extraSmall.copy(
bottomStart = MaterialTheme.shapes.large.bottomStart,
bottomEnd = MaterialTheme.shapes.large.bottomEnd
)
else -> MaterialTheme.shapes.extraSmall
}
)
.then(modifier)
)
}
@Composable @Composable
fun SettingsIcon(danger: Boolean = false, content: @Composable () -> Unit) { fun SettingsIcon(danger: Boolean = false, content: @Composable () -> Unit) {
CompositionLocalProvider( CompositionLocalProvider(
@ -454,4 +434,4 @@ fun SettingsIcon(danger: Boolean = false, content: @Composable () -> Unit) {
) { ) {
content() content()
} }
} }

View File

@ -4,11 +4,12 @@ import android.app.Application
import android.util.Log import android.util.Log
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -311,47 +312,58 @@ fun MemberListSheet(
) )
} }
is MemberListSheetItem.MemberItem -> item(key = item.member.id!!.user) { is MemberListSheetItem.MemberItem -> {
MemberListItem( val isFirst = index == 0 || viewModel.fullItemList[index - 1] is MemberListSheetItem.CategoryItem
user = StoatAPI.userCache[item.member.id!!.user], val isLast = index == viewModel.fullItemList.size - 1 || viewModel.fullItemList[index + 1] is MemberListSheetItem.CategoryItem
member = item.member, item(key = item.member.id!!.user) {
serverId = serverId, MemberListItem(
userId = item.member.id!!.user, user = StoatAPI.userCache[item.member.id!!.user],
modifier = Modifier member = item.member,
.combinedClickable( serverId = serverId,
onClick = { userId = item.member.id!!.user,
userInfoSheetTarget = item.member.id!!.user first = isFirst,
showUserInfoSheet = true last = isLast,
}, onClick = {
onClickLabel = stringResource(R.string.user_info_sheet_open), userInfoSheetTarget = item.member.id!!.user
onLongClick = { showUserInfoSheet = true
memberContextSheetTarget = item.member.id!!.user },
showMemberContextSheet = true onLongClick = {
}, memberContextSheetTarget = item.member.id!!.user
onLongClickLabel = stringResource(R.string.member_context_sheet_open) showMemberContextSheet = true
) },
) modifier = Modifier.padding(horizontal = 16.dp)
)
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
}
} }
is MemberListSheetItem.UserItem -> item(key = item.user.id!!) { is MemberListSheetItem.UserItem -> {
MemberListItem( val isFirst = index == 0 || viewModel.fullItemList[index - 1] is MemberListSheetItem.CategoryItem
user = item.user, val isLast = index == viewModel.fullItemList.size - 1 || viewModel.fullItemList[index + 1] is MemberListSheetItem.CategoryItem
member = null, item(key = item.user.id!!) {
serverId = serverId, MemberListItem(
userId = item.user.id!!, user = item.user,
modifier = Modifier.combinedClickable( member = null,
serverId = serverId,
userId = item.user.id!!,
first = isFirst,
last = isLast,
onClick = { onClick = {
userInfoSheetTarget = item.user.id!! userInfoSheetTarget = item.user.id!!
showUserInfoSheet = true showUserInfoSheet = true
}, },
onClickLabel = stringResource(R.string.user_info_sheet_open),
onLongClick = { onLongClick = {
memberContextSheetTarget = item.user.id!! memberContextSheetTarget = item.user.id!!
showMemberContextSheet = true showMemberContextSheet = true
}, },
onLongClickLabel = stringResource(R.string.member_context_sheet_open) modifier = Modifier.padding(horizontal = 16.dp)
) )
) if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
}
} }
} }
} }

View File

@ -14,7 +14,7 @@ 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.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -218,9 +218,9 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding( modifier = Modifier.padding(
top = 16.dp, top = 16.dp,
start = 16.dp, start = 24.dp,
end = 16.dp, end = 24.dp,
bottom = 4.dp bottom = 16.dp
), ),
) { ) {
Row( Row(
@ -308,15 +308,14 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
) )
} }
} }
HorizontalDivider()
} }
} }
} }
val reactionsForEmoji = val reactionsForEmoji =
reactions?.get(reactionEmoji[selectedReactionIndex]) ?: emptyList() reactions?.get(reactionEmoji[selectedReactionIndex]) ?: emptyList()
items(items = reactionsForEmoji) { reaction -> itemsIndexed(items = reactionsForEmoji) { index, reaction ->
val isLast = index == reactionsForEmoji.size - 1
val userOrNull = StoatAPI.userCache[reaction] val userOrNull = StoatAPI.userCache[reaction]
val user = userOrNull ?: User.getPlaceholder(reaction) val user = userOrNull ?: User.getPlaceholder(reaction)
val member = if (channel.server != null && user.id != null) { val member = if (channel.server != null && user.id != null) {
@ -340,7 +339,13 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
user = user, user = user,
serverId = channel.server, serverId = channel.server,
userId = reaction, userId = reaction,
first = index == 0,
last = isLast,
modifier = Modifier.padding(horizontal = 16.dp)
) )
if (!isLast) {
Spacer(modifier = Modifier.height(2.dp))
}
} }
item("bottom") { item("bottom") {