feat: display role icons

This commit is contained in:
infi 2026-06-20 16:10:53 +02:00
parent 55dbb2a8e5
commit 126011a3e3
5 changed files with 101 additions and 25 deletions

View File

@ -13,12 +13,12 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -28,9 +28,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.stoat.R import chat.stoat.R
import chat.stoat.api.StoatAPI import chat.stoat.api.StoatAPI
import chat.stoat.api.internals.SpecialUsers
import chat.stoat.api.internals.solidColor import chat.stoat.api.internals.solidColor
import chat.stoat.api.routes.channel.fetchSingleMessage import chat.stoat.api.routes.channel.fetchSingleMessage
import chat.stoat.composables.generic.RemoteImage
import chat.stoat.composables.generic.UserAvatar import chat.stoat.composables.generic.UserAvatar
import chat.stoat.core.model.data.STOAT_FILES
import chat.stoat.core.model.schemas.User import chat.stoat.core.model.schemas.User
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
@ -52,6 +55,7 @@ fun InReplyTo(
val contentColor = LocalContentColor.current val contentColor = LocalContentColor.current
val usernameColor = val usernameColor =
message?.let { authorColour(it) } ?: Brush.solidColor(contentColor) message?.let { authorColour(it) } ?: Brush.solidColor(contentColor)
val roleIcon = message?.let { authorRoleIcon(it) }
val serverId = remember(channelId) { StoatAPI.channelCache[channelId]?.server } val serverId = remember(channelId) { StoatAPI.channelCache[channelId]?.server }
@ -107,11 +111,26 @@ fun InReplyTo(
modifier = Modifier.padding(horizontal = 4.dp) modifier = Modifier.padding(horizontal = 4.dp)
) )
if (roleIcon != null) {
RemoteImage(
url = "$STOAT_FILES/icons/${roleIcon.id}",
contentScale = ContentScale.Fit,
description = null,
modifier = Modifier
.size(12.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
InlineBadges( InlineBadges(
bot = message.masquerade == null && author?.bot != null, bot = message.masquerade == null && author?.bot != null,
bridge = message.masquerade != null && author?.bot != null, bridge = message.masquerade != null && author?.bot != null,
platformModeration = author?.id == SpecialUsers.PLATFORM_MODERATION_USER,
teamMember = author?.id in SpecialUsers.TEAM_MEMBER_FLAIRS.keys,
webhook = message.webhook != null,
colour = contentColor.copy(alpha = 0.5f), colour = contentColor.copy(alpha = 0.5f),
modifier = Modifier.size(8.dp), modifier = Modifier.size(12.dp),
followingIfAny = { followingIfAny = {
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
} }

View File

@ -5,7 +5,6 @@ import android.content.Intent
import android.icu.text.DateFormat import android.icu.text.DateFormat
import android.net.Uri import android.net.Uri
import android.text.format.DateUtils import android.text.format.DateUtils
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
@ -111,6 +110,17 @@ fun authorColour(message: MessageSchema): Brush {
} }
} }
@Composable
fun authorRoleIcon(message: MessageSchema): AutumnResource? {
val serverId = StoatAPI.channelCache[message.channel]?.server ?: return null
val highestRole = message.author?.let {
Roles.resolveHighestRole(serverId, it)
} ?: return null
return highestRole.icon
}
@Composable @Composable
fun displayNameInChannel(userId: String, channelId: String): String { fun displayNameInChannel(userId: String, channelId: String): String {
val serverId = val serverId =
@ -321,9 +331,6 @@ fun Message(
} == true), } == true),
) { ) {
// TODO Add jump to message // TODO Add jump to message
if (replyMessage == null) {
Toast.makeText(context, "lmao prankd", Toast.LENGTH_SHORT).show()
}
} }
} }
} }
@ -399,6 +406,19 @@ fun Message(
) )
) )
val roleIcon = authorRoleIcon(message)
if (roleIcon != null) {
Spacer(modifier = Modifier.width(4.dp))
RemoteImage(
url = "$STOAT_FILES/icons/${roleIcon.id}",
contentScale = ContentScale.Fit,
description = null,
modifier = Modifier
.size(16.dp)
)
}
InlineBadges( InlineBadges(
bot = author.bot != null && message.masquerade == null, bot = author.bot != null && message.masquerade == null,
bridge = message.masquerade != null && author.bot != null, bridge = message.masquerade != null && author.bot != null,
@ -520,9 +540,12 @@ fun Message(
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Embed(embed = embed, serverId = StoatAPI.channelCache[message.channel]?.server, onLinkClick = { Embed(
viewUrlInBrowser(context, it) embed = embed,
}) serverId = StoatAPI.channelCache[message.channel]?.server,
onLinkClick = {
viewUrlInBrowser(context, it)
})
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }

View File

@ -2,6 +2,7 @@ package chat.stoat.composables.generic
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
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.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -39,23 +40,29 @@ fun ListHeader(
fun CountableListHeader( fun CountableListHeader(
text: String, text: String,
count: Int, count: Int,
icon: @Composable (() -> Unit)? = null,
backgroundColor: Color = MaterialTheme.colorScheme.surface backgroundColor: Color = MaterialTheme.colorScheme.surface
) { ) {
ListHeader(backgroundColor = backgroundColor) { ListHeader(backgroundColor = backgroundColor) {
Text( Row {
text = AnnotatedString.Builder().apply { if (icon != null) {
append(text) icon()
}
Text(
text = AnnotatedString.Builder().apply {
append(text)
pushStyle( pushStyle(
SpanStyle( SpanStyle(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = LocalTextStyle.current.fontSize * 0.8, fontSize = LocalTextStyle.current.fontSize * 0.8,
color = LocalContentColor.current.copy(alpha = 0.6f) color = LocalContentColor.current.copy(alpha = 0.6f)
)
) )
) append("$count")
append("$count") pop()
pop() }.toAnnotatedString()
}.toAnnotatedString() )
) }
} }
} }

View File

@ -10,6 +10,7 @@ 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.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@ -28,6 +29,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow 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.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -42,8 +44,11 @@ import chat.stoat.api.routes.server.fetchMembers
import chat.stoat.composables.chat.MemberListItem import chat.stoat.composables.chat.MemberListItem
import chat.stoat.composables.generic.CountableListHeader import chat.stoat.composables.generic.CountableListHeader
import chat.stoat.composables.generic.Presence import chat.stoat.composables.generic.Presence
import chat.stoat.composables.generic.RemoteImage
import chat.stoat.composables.generic.SheetHeaderPadding import chat.stoat.composables.generic.SheetHeaderPadding
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.schemas.AutumnResource
import chat.stoat.core.model.schemas.Member import chat.stoat.core.model.schemas.Member
import chat.stoat.core.model.schemas.User import chat.stoat.core.model.schemas.User
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -57,7 +62,11 @@ val DO_NOT_FETCH_OFFLINE_MEMBERS_SERVERS = listOf(
sealed class MemberListSheetItem { sealed class MemberListSheetItem {
data class MemberItem(val member: Member) : MemberListSheetItem() data class MemberItem(val member: Member) : MemberListSheetItem()
data class UserItem(val user: User) : MemberListSheetItem() data class UserItem(val user: User) : MemberListSheetItem()
data class CategoryItem(val category: String, val count: Int) : MemberListSheetItem() data class CategoryItem(
val category: String,
val count: Int,
val icon: AutumnResource? = null
) : MemberListSheetItem()
} }
class MemberListSheetViewModel( class MemberListSheetViewModel(
@ -116,7 +125,13 @@ class MemberListSheetViewModel(
// Hoisted roles // Hoisted roles
Roles.inOrder(serverId) { it.hoist == true }.forEach { role -> Roles.inOrder(serverId) { it.hoist == true }.forEach { role ->
val members = categories[role.name] ?: return@forEach val members = categories[role.name] ?: return@forEach
fullItemList.add(MemberListSheetItem.CategoryItem(role.name ?: "", members.size)) fullItemList.add(
MemberListSheetItem.CategoryItem(
role.name ?: "",
members.size,
role.icon
)
)
members.forEach { member -> members.forEach { member ->
fullItemList.add(MemberListSheetItem.MemberItem(member)) fullItemList.add(MemberListSheetItem.MemberItem(member))
} }
@ -312,6 +327,17 @@ fun MemberListSheet(
CountableListHeader( CountableListHeader(
text = item.category, text = item.category,
count = item.count, count = item.count,
icon = {
if (item.icon == null) return@CountableListHeader
RemoteImage(
url = "$STOAT_FILES/icons/${item.icon.id}",
contentScale = ContentScale.Fit,
description = null,
modifier = Modifier
.padding(end = 8.dp)
.size(16.dp)
)
},
backgroundColor = MaterialTheme.colorScheme.surfaceContainerLow backgroundColor = MaterialTheme.colorScheme.surfaceContainerLow
) )
} }

View File

@ -74,7 +74,8 @@ data class Role(
val permissions: PermissionDescription? = null, val permissions: PermissionDescription? = null,
val colour: String? = null, val colour: String? = null,
val hoist: Boolean? = null, val hoist: Boolean? = null,
val rank: Double? = null val rank: Double? = null,
val icon: AutumnResource? = null
) { ) {
fun mergeWithPartial(other: Role): Role { fun mergeWithPartial(other: Role): Role {
return Role( return Role(