feat: colourblind friendly status indicators

This commit is contained in:
infi 2026-08-04 13:47:39 +02:00
parent 667449fc0b
commit 4a51c7c212
7 changed files with 133 additions and 9 deletions

View File

@ -2,31 +2,37 @@ package chat.stoat.composables.generic
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import chat.stoat.R
import chat.stoat.core.model.schemas.AutumnResource
import chat.stoat.api.settings.LoadedSettings
import chat.stoat.core.model.data.STOAT_BASE
import chat.stoat.core.model.data.STOAT_FILES
import chat.stoat.core.model.schemas.AutumnResource
enum class Presence {
Online,
@ -68,17 +74,61 @@ fun presenceColour(presence: Presence): Color {
}
}
private fun Presence.iconResource(): Int = when (this) {
Presence.Online -> R.drawable.ic_circle_24dp__fill
Presence.Idle -> R.drawable.ic_bedtime_24dp
Presence.Dnd -> R.drawable.ic_do_not_disturb_on_24dp
Presence.Focus -> R.drawable.ic_circle_circle_24dp
Presence.Offline -> R.drawable.ic_circle_24dp
}
private fun Presence.descriptionResource(): Int = when (this) {
Presence.Online -> R.string.status_online
Presence.Idle -> R.string.status_idle
Presence.Dnd -> R.string.status_dnd
Presence.Focus -> R.string.status_focus
Presence.Offline -> R.string.status_offline
}
@Composable
fun PresenceBadge(presence: Presence, size: Dp = 16.dp) {
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
.border(2.dp, MaterialTheme.colorScheme.background, CircleShape)
.background(presenceColour(presence))
)
contentAlignment = Alignment.Center,
modifier = Modifier.size(size)
) {
Icon(
painter = painterResource(presence.iconResource()),
contentDescription = stringResource(presence.descriptionResource()),
tint = presenceColour(presence),
modifier = Modifier
.size((size - PresenceCutoutPadding * 2).coerceAtLeast(0.dp))
)
}
}
private val PresenceCutoutPadding = 2.dp
private fun Modifier.presenceCutout(diameter: Dp): Modifier =
graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}.drawWithCache {
val radius = diameter.toPx() / 2
val center = Offset(
x = if (layoutDirection == LayoutDirection.Ltr) size.width - radius else radius,
y = size.height - radius
)
onDrawWithContent {
drawContent()
drawCircle(
color = Color.Transparent,
radius = radius,
center = center,
blendMode = BlendMode.Clear
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun UserAvatar(
@ -109,6 +159,13 @@ fun UserAvatar(
modifier = Modifier
.clip(shape)
.size(size)
.then(
if (presence != null) {
Modifier.presenceCutout(presenceSize)
} else {
Modifier
}
)
.then(
if (onLongClick != null || onClick != null) {
Modifier
@ -129,6 +186,13 @@ fun UserAvatar(
modifier = Modifier
.clip(shape)
.size(size)
.then(
if (presence != null) {
Modifier.presenceCutout(presenceSize)
} else {
Modifier
}
)
.then(
if (onLongClick != null || onClick != null) {
Modifier

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M565,565Q600,530 600,480Q600,430 565,395Q530,360 480,360Q430,360 395,395Q360,430 360,480Q360,530 395,565Q430,600 480,600Q530,600 565,565ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M484,880Q400,880 326.5,848Q253,816 198.5,761.5Q144,707 112,633.5Q80,560 80,476Q80,330 173,218.5Q266,107 410,80Q392,179 421,273.5Q450,368 521,439Q592,510 686.5,539Q781,568 880,550Q854,694 742,787Q630,880 484,880Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Q397,880 324,848.5ZM621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280Q397,280 338.5,338.5Q280,397 280,480Q280,563 338.5,621.5Q397,680 480,680Q563,680 621.5,621.5Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,520L680,520L680,440L280,440L280,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
</vector>