diff --git a/app/src/main/java/chat/revolt/api/internals/WebCompat.kt b/app/src/main/java/chat/revolt/api/internals/WebCompat.kt
index d4a22e4a..1071c8f2 100644
--- a/app/src/main/java/chat/revolt/api/internals/WebCompat.kt
+++ b/app/src/main/java/chat/revolt/api/internals/WebCompat.kt
@@ -7,14 +7,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Brush.Companion.linearGradient
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
-// color is spelled american because Color from compose is spelled american
-fun Brush.Companion.solidColor(colour: Color) = linearGradient(
- colorStops = arrayOf(
- 0f to colour,
- 1f to colour
- )
-)
+fun Brush.Companion.solidColor(colour: Color) = SolidColor(colour)
// Some colours that are not present in Android's built-in list.
// not exhaustive, but covers most of the ones I've seen in the wild
diff --git a/app/src/main/java/chat/revolt/api/schemas/User.kt b/app/src/main/java/chat/revolt/api/schemas/User.kt
index 9d460d2a..5ef061d3 100644
--- a/app/src/main/java/chat/revolt/api/schemas/User.kt
+++ b/app/src/main/java/chat/revolt/api/schemas/User.kt
@@ -65,6 +65,25 @@ data class User(
}
}
+enum class UserBadges(val value: Long) {
+ Developer(1L shl 0),
+ Translator(1L shl 1),
+ Supporter(1L shl 2),
+ ResponsibleDisclosure(1L shl 3),
+ Founder(1L shl 4),
+ PlatformModeration(1L shl 5),
+ ActiveSupporter(1L shl 6),
+ Paw(1L shl 7),
+ EarlyAdopter(1L shl 8),
+ ReservedRelevantJokeBadge1(1L shl 9),
+ ReservedRelevantJokeBadge2(1L shl 10),
+}
+
+infix fun Long?.has(flag: UserBadges): Boolean {
+ if (this == null) return false
+ return this and flag.value == flag.value
+}
+
@Serializable
data class Bot(
val owner: String? = null
diff --git a/app/src/main/java/chat/revolt/components/chat/RoleListEntry.kt b/app/src/main/java/chat/revolt/components/chat/RoleListEntry.kt
new file mode 100644
index 00000000..0016c49d
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/chat/RoleListEntry.kt
@@ -0,0 +1,39 @@
+package chat.revolt.components.chat
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun RoleListEntry(label: String, brush: Brush, modifier: Modifier = Modifier) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ ) {
+ Text(
+ text = label,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(Modifier.weight(1f))
+
+ Box(
+ modifier = Modifier
+ .clip(CircleShape)
+ .size(14.dp)
+ .background(brush = brush)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/components/chat/UserBadgeList.kt b/app/src/main/java/chat/revolt/components/chat/UserBadgeList.kt
new file mode 100644
index 00000000..3010fcf4
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/chat/UserBadgeList.kt
@@ -0,0 +1,184 @@
+package chat.revolt.components.chat
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import chat.revolt.R
+import chat.revolt.api.schemas.UserBadges
+import chat.revolt.api.schemas.has
+
+@Composable
+fun BadgeListEntryTemplate(
+ label: String,
+ icon: Painter
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = icon,
+ contentDescription = null,
+ modifier = Modifier
+ .size(24.dp)
+ )
+
+ Spacer(Modifier.width(8.dp))
+
+ Text(
+ text = label
+ )
+ }
+}
+
+@Composable
+fun BadgeListEntry(badge: UserBadges) {
+ when (badge.value) {
+ UserBadges.Developer.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_developer),
+ icon = painterResource(R.drawable.user_badge_developer)
+ )
+ }
+
+ UserBadges.Translator.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_translator),
+ icon = painterResource(R.drawable.user_badge_translator)
+ )
+ }
+
+ UserBadges.Supporter.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_supporter),
+ icon = painterResource(R.drawable.user_badge_supporter)
+ )
+ }
+
+ UserBadges.ResponsibleDisclosure.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_responsible_disclosure),
+ icon = painterResource(R.drawable.user_badge_disclosure)
+ )
+ }
+
+ UserBadges.Founder.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_founder),
+ icon = painterResource(R.drawable.user_badge_founder)
+ )
+ }
+
+ UserBadges.PlatformModeration.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_platform_moderation),
+ icon = painterResource(R.drawable.user_badge_moderation)
+ )
+ }
+
+ UserBadges.ActiveSupporter.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_active_supporter),
+ icon = painterResource(R.drawable.ic_human_greeting_variant_24dp)
+ )
+ }
+
+ UserBadges.Paw.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_paw),
+ icon = painterResource(R.drawable.user_badge_paw)
+ )
+ }
+
+ UserBadges.EarlyAdopter.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_early_adopter),
+ icon = painterResource(R.drawable.user_badge_early_adopter)
+ )
+ }
+
+ UserBadges.ReservedRelevantJokeBadge1.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_reserved_relevant_joke_badge_1),
+ icon = painterResource(R.drawable.user_badge_reserved_relevant_one)
+ )
+ }
+
+ UserBadges.ReservedRelevantJokeBadge2.value -> {
+ BadgeListEntryTemplate(
+ label = stringResource(R.string.user_badge_reserved_relevant_joke_badge_2),
+ icon = painterResource(R.drawable.user_badge_reserved_relevant_two)
+ )
+ }
+ }
+}
+
+@Composable
+fun UserBadgeList(badges: Long) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ UserBadges.entries
+ .filter { badges has it }
+ .forEach { badge ->
+ BadgeListEntry(badge)
+ }
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun UserBadgeRow(badges: Long) {
+ FlowRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ UserBadges.entries
+ .filter { badges has it }
+ .forEach { badge ->
+ Image(
+ painter = when (badge) {
+ UserBadges.Developer -> painterResource(R.drawable.user_badge_developer)
+ UserBadges.Translator -> painterResource(R.drawable.user_badge_translator)
+ UserBadges.Supporter -> painterResource(R.drawable.user_badge_supporter)
+ UserBadges.ResponsibleDisclosure -> painterResource(R.drawable.user_badge_disclosure)
+ UserBadges.Founder -> painterResource(R.drawable.user_badge_founder)
+ UserBadges.PlatformModeration -> painterResource(R.drawable.user_badge_moderation)
+ UserBadges.ActiveSupporter -> painterResource(R.drawable.ic_human_greeting_variant_24dp)
+ UserBadges.Paw -> painterResource(R.drawable.user_badge_paw)
+ UserBadges.EarlyAdopter -> painterResource(R.drawable.user_badge_early_adopter)
+ UserBadges.ReservedRelevantJokeBadge1 -> painterResource(R.drawable.user_badge_reserved_relevant_one)
+ UserBadges.ReservedRelevantJokeBadge2 -> painterResource(R.drawable.user_badge_reserved_relevant_two)
+ },
+ contentDescription = when (badge) {
+ UserBadges.Developer -> stringResource(R.string.user_badge_developer)
+ UserBadges.Translator -> stringResource(R.string.user_badge_translator)
+ UserBadges.Supporter -> stringResource(R.string.user_badge_supporter)
+ UserBadges.ResponsibleDisclosure -> stringResource(R.string.user_badge_responsible_disclosure)
+ UserBadges.Founder -> stringResource(R.string.user_badge_founder)
+ UserBadges.PlatformModeration -> stringResource(R.string.user_badge_platform_moderation)
+ UserBadges.ActiveSupporter -> stringResource(R.string.user_badge_active_supporter)
+ UserBadges.Paw -> stringResource(R.string.user_badge_paw)
+ UserBadges.EarlyAdopter -> stringResource(R.string.user_badge_early_adopter)
+ UserBadges.ReservedRelevantJokeBadge1 -> stringResource(R.string.user_badge_reserved_relevant_joke_badge_1)
+ UserBadges.ReservedRelevantJokeBadge2 -> stringResource(R.string.user_badge_reserved_relevant_joke_badge_2)
+ },
+ modifier = Modifier
+ .size(32.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/components/screens/settings/UserButtons.kt b/app/src/main/java/chat/revolt/components/screens/settings/UserButtons.kt
index 3bb28df2..30ae6620 100644
--- a/app/src/main/java/chat/revolt/components/screens/settings/UserButtons.kt
+++ b/app/src/main/java/chat/revolt/components/screens/settings/UserButtons.kt
@@ -11,7 +11,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -100,7 +100,7 @@ fun UserButtons(
}
"Friend" -> {
- ElevatedButton(
+ FilledTonalButton(
onClick = {
scope.launch {
val dm = openDM(user.id)
diff --git a/app/src/main/java/chat/revolt/components/sheets/SheetTile.kt b/app/src/main/java/chat/revolt/components/sheets/SheetTile.kt
new file mode 100644
index 00000000..7f47910e
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/sheets/SheetTile.kt
@@ -0,0 +1,85 @@
+package chat.revolt.components.sheets
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import chat.revolt.api.internals.solidColor
+
+@Composable
+fun SheetTile(
+ modifier: Modifier = Modifier,
+ header: @Composable () -> Unit,
+ contentPreview: @Composable () -> Unit,
+ clickable: Boolean = true,
+ backgroundBrush: Brush = Brush.solidColor(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)),
+ content: @Composable () -> Unit,
+) {
+ var isExpanded by remember { mutableStateOf(false) }
+
+ if (isExpanded) {
+ Dialog(onDismissRequest = {
+ isExpanded = false
+ }) {
+ BoxWithConstraints {
+ Column(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.surface)
+ .padding(24.dp)
+ .width(maxWidth * 0.85f)
+ .heightIn(max = maxHeight * 0.85f)
+ ) {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.headlineMedium) {
+ header()
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {
+ content()
+ }
+ }
+ }
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.large)
+ .then(if (clickable) Modifier.clickable { isExpanded = true } else Modifier)
+ .background(backgroundBrush)
+ .padding(16.dp)
+ .height(128.dp)
+ .then(modifier)
+ ) {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.headlineSmall) {
+ header()
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {
+ contentPreview()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/sheets/UserInfoSheet.kt b/app/src/main/java/chat/revolt/sheets/UserInfoSheet.kt
index a6e568e8..e845c921 100644
--- a/app/src/main/java/chat/revolt/sheets/UserInfoSheet.kt
+++ b/app/src/main/java/chat/revolt/sheets/UserInfoSheet.kt
@@ -1,16 +1,15 @@
package chat.revolt.sheets
+import android.text.format.DateUtils
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@@ -21,25 +20,30 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import chat.revolt.R
import chat.revolt.api.RevoltAPI
+import chat.revolt.api.internals.ULID
import chat.revolt.api.internals.WebCompat
import chat.revolt.api.internals.solidColor
import chat.revolt.api.routes.user.fetchUserProfile
import chat.revolt.api.schemas.Profile
-import chat.revolt.components.chat.RoleChip
+import chat.revolt.components.chat.RoleListEntry
+import chat.revolt.components.chat.UserBadgeList
+import chat.revolt.components.chat.UserBadgeRow
import chat.revolt.components.generic.NonIdealState
import chat.revolt.components.generic.WebMarkdown
import chat.revolt.components.screens.settings.RawUserOverview
import chat.revolt.components.screens.settings.UserButtons
+import chat.revolt.components.sheets.SheetTile
+import kotlinx.datetime.Instant
-@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UserInfoSheet(
userId: String,
@@ -90,72 +94,174 @@ fun UserInfoSheet(
return
}
- Column(
- modifier = Modifier
- .verticalScroll(rememberScrollState())
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(2),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalItemSpacing = 16.dp,
+ modifier = Modifier.padding(16.dp)
) {
- RawUserOverview(user, profile)
+ item(key = "overview", span = StaggeredGridItemSpan.FullLine) {
+ RawUserOverview(user, profile, internalPadding = false)
+ }
- Column(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 8.dp)
- ) {
- UserButtons(
- user,
- dismissSheet
- )
-
- member?.roles?.let {
- Text(
- text = stringResource(id = R.string.user_info_sheet_category_roles),
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(vertical = 10.dp)
- )
-
- FlowRow(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- it
- .map { roleId -> server?.roles?.get(roleId) }
- .sortedBy { it?.rank ?: 0.0 }
- .forEach { role ->
- role?.let {
- RoleChip(
- label = role.name ?: "null",
- brush = role.colour?.let { WebCompat.parseColour(it) }
- ?: Brush.solidColor(LocalContentColor.current)
- )
- }
+ member?.roles?.let {
+ item(key = "roles") {
+ SheetTile(
+ header = {
+ Text(stringResource(R.string.user_info_sheet_category_roles))
+ },
+ contentPreview = {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ it
+ .map { roleId -> server?.roles?.get(roleId) }
+ .sortedBy { it?.rank ?: 0.0 }
+ .take(3)
+ .forEach { role ->
+ role?.let {
+ RoleListEntry(
+ label = role.name ?: "null",
+ brush = role.colour?.let { WebCompat.parseColour(it) }
+ ?: Brush.solidColor(LocalContentColor.current)
+ )
+ }
+ }
}
- }
- }
-
- Text(
- text = stringResource(id = R.string.user_info_sheet_category_bio),
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(vertical = 10.dp)
- )
-
- if (profile?.content.isNullOrBlank().not()) {
- WebMarkdown(
- text = profile!!.content!!,
- maskLoading = true
- )
- } else if (profile != null) {
- Text(
- text = stringResource(id = R.string.user_info_sheet_bio_empty),
- color = LocalContentColor.current.copy(alpha = 0.6f)
- )
- } else if (profileNotFound) {
- Text(
- text = stringResource(id = R.string.user_info_sheet_bio_not_found),
- color = LocalContentColor.current.copy(alpha = 0.6f)
- )
- } else {
- Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
- CircularProgressIndicator()
+ }
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ it
+ .map { roleId -> server?.roles?.get(roleId) }
+ .sortedBy { it?.rank ?: 0.0 }
+ .forEach { role ->
+ role?.let {
+ RoleListEntry(
+ label = role.name ?: "null",
+ brush = role.colour?.let { WebCompat.parseColour(it) }
+ ?: Brush.solidColor(LocalContentColor.current)
+ )
+ }
+ }
+ }
}
}
}
+ val accountAt = user.id?.let {
+ DateUtils.getRelativeTimeSpanString(
+ ULID.asTimestamp(user.id),
+ System.currentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS
+ ).toString()
+ }
+ val joinedAt = member?.joinedAt?.let {
+ DateUtils.getRelativeTimeSpanString(
+ Instant.parse(member.joinedAt).toEpochMilliseconds(),
+ System.currentTimeMillis(),
+ DateUtils.MINUTE_IN_MILLIS
+ ).toString()
+ }
+
+ item(key = "joined") {
+ SheetTile(
+ header = {
+ Text(stringResource(R.string.user_info_sheet_category_joined))
+ },
+ contentPreview = {
+ if (joinedAt != null && server?.name != null) {
+ Text(
+ text = joinedAt,
+ fontSize = 14.sp
+ )
+
+ Text(
+ text = server.name,
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Spacer(Modifier.height(8.dp))
+ }
+
+ accountAt?.let { _ ->
+ Text(
+ text = accountAt,
+ fontSize = 14.sp
+ )
+
+ Text(
+ text = stringResource(id = R.string.user_info_sheet_category_joined_revolt),
+ style = MaterialTheme.typography.labelMedium
+ )
+ }
+ }
+ ) {
+ if (joinedAt != null && server?.name != null) {
+ Text(
+ text = joinedAt,
+ style = MaterialTheme.typography.displaySmall
+ )
+
+ Text(
+ text = server.name,
+ style = MaterialTheme.typography.labelMedium
+ )
+
+ Spacer(Modifier.height(8.dp))
+ }
+
+ accountAt?.let { _ ->
+ Text(
+ text = accountAt,
+ style = MaterialTheme.typography.displaySmall
+ )
+
+ Text(
+ text = stringResource(id = R.string.user_info_sheet_category_joined_revolt),
+ style = MaterialTheme.typography.labelMedium
+ )
+ }
+ }
+ }
+
+ if ((user.badges ?: 0) > 0) {
+ item(key = "info") {
+ SheetTile(
+ header = {
+ Text(stringResource(R.string.user_info_sheet_category_badges))
+ },
+ contentPreview = {
+ user.badges?.let { UserBadgeRow(badges = it) }
+ }
+ ) {
+ user.badges?.let { UserBadgeList(badges = it) }
+ }
+ }
+ }
+
+ if (profile?.content.isNullOrBlank().not()) {
+ item(key = "bio", span = StaggeredGridItemSpan.FullLine) {
+ SheetTile(
+ header = {
+ Text(stringResource(R.string.user_info_sheet_category_bio))
+ },
+ contentPreview = {
+ Text(
+ text = profile?.content!!,
+ maxLines = 4,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ ) {
+ WebMarkdown(
+ text = profile?.content!!,
+ maskLoading = true
+ )
+ }
+ }
+ }
+
+ item(key = "actions", span = StaggeredGridItemSpan.FullLine) {
+ UserButtons(user, dismissSheet)
+ }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/user_badge_developer.xml b/app/src/main/res/drawable/user_badge_developer.xml
new file mode 100644
index 00000000..e78a8601
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_developer.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_disclosure.xml b/app/src/main/res/drawable/user_badge_disclosure.xml
new file mode 100644
index 00000000..720b5934
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_disclosure.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_early_adopter.xml b/app/src/main/res/drawable/user_badge_early_adopter.xml
new file mode 100644
index 00000000..cca7f8c9
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_early_adopter.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_founder.xml b/app/src/main/res/drawable/user_badge_founder.xml
new file mode 100644
index 00000000..e5965c51
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_founder.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_moderation.xml b/app/src/main/res/drawable/user_badge_moderation.xml
new file mode 100644
index 00000000..e3215f66
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_moderation.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_paw.xml b/app/src/main/res/drawable/user_badge_paw.xml
new file mode 100644
index 00000000..625a5538
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_paw.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_raccoon.xml b/app/src/main/res/drawable/user_badge_raccoon.xml
new file mode 100644
index 00000000..148c4849
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_raccoon.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_reserved_relevant_one.xml b/app/src/main/res/drawable/user_badge_reserved_relevant_one.xml
new file mode 100644
index 00000000..408a14a8
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_reserved_relevant_one.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_reserved_relevant_two.xml b/app/src/main/res/drawable/user_badge_reserved_relevant_two.xml
new file mode 100644
index 00000000..1f8620be
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_reserved_relevant_two.xml
@@ -0,0 +1,2601 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_supporter.xml b/app/src/main/res/drawable/user_badge_supporter.xml
new file mode 100644
index 00000000..4c6c2af6
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_supporter.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/user_badge_translator.xml b/app/src/main/res/drawable/user_badge_translator.xml
new file mode 100644
index 00000000..2f0956cb
--- /dev/null
+++ b/app/src/main/res/drawable/user_badge_translator.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 09e9386e..c861b9db 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -277,6 +277,10 @@
This user hasn\'t set a bio yet.
This user\'s bio could not be fetched. Please verify you share a server or are friends.
Roles
+ Joined
+ Revolt
+ Badges
+ Bot
Add Friend
Send Message
Remove Friend
@@ -290,6 +294,17 @@
Copy ID
Could not open DM with this user.
+ Developer
+ Translator
+ Supporter
+ Responsible Disclosure
+ Founder
+ Platform Moderation
+ Active Supporter
+ 🦊
+ Early Adopter
+ sus
+ It\'s Morbin Time
Add a server
Join by invite code or link