diff --git a/app/src/main/java/chat/revolt/api/schemas/Settings.kt b/app/src/main/java/chat/revolt/api/schemas/Settings.kt
index f3c18595..beabd9e8 100644
--- a/app/src/main/java/chat/revolt/api/schemas/Settings.kt
+++ b/app/src/main/java/chat/revolt/api/schemas/Settings.kt
@@ -25,4 +25,9 @@ data class AndroidSpecificSettings(
* Can be one of `{ None, SwipeFromEnd, DoubleTap }`
*/
var messageReplyStyle: String? = null,
+ /**
+ * Avatar radius.
+ * Must be integer in range 0..50 inclusive.
+ */
+ var avatarRadius: Int? = null
)
diff --git a/app/src/main/java/chat/revolt/api/settings/GlobalState.kt b/app/src/main/java/chat/revolt/api/settings/GlobalState.kt
index 044108c1..6a4ce030 100644
--- a/app/src/main/java/chat/revolt/api/settings/GlobalState.kt
+++ b/app/src/main/java/chat/revolt/api/settings/GlobalState.kt
@@ -1,6 +1,7 @@
package chat.revolt.api.settings
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import chat.revolt.ui.theme.Theme
@@ -15,16 +16,19 @@ enum class MessageReplyStyle {
object GlobalState {
var theme by mutableStateOf(getDefaultTheme())
var messageReplyStyle by mutableStateOf(MessageReplyStyle.SwipeFromEnd)
+ var avatarRadius by mutableIntStateOf(50)
fun hydrateWithSettings(settings: SyncedSettings) {
this.theme = settings.android.theme?.let { Theme.valueOf(it) } ?: getDefaultTheme()
this.messageReplyStyle =
settings.android.messageReplyStyle?.let { MessageReplyStyle.valueOf(it) }
?: MessageReplyStyle.SwipeFromEnd
+ this.avatarRadius = settings.android.avatarRadius ?: 50
}
fun reset() {
theme = getDefaultTheme()
messageReplyStyle = MessageReplyStyle.SwipeFromEnd
+ avatarRadius = 50
}
}
diff --git a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt
index a1bad473..99d55d5b 100644
--- a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt
+++ b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt
@@ -8,6 +8,7 @@ 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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -24,6 +25,7 @@ import chat.revolt.R
import chat.revolt.api.REVOLT_BASE
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.schemas.AutumnResource
+import chat.revolt.api.settings.GlobalState
enum class Presence {
Online,
@@ -101,7 +103,7 @@ fun UserAvatar(
contentScale = ContentScale.Crop,
description = stringResource(id = R.string.avatar_alt, username),
modifier = Modifier
- .clip(CircleShape)
+ .clip(RoundedCornerShape(GlobalState.avatarRadius))
.size(size)
.then(
if (onLongClick != null || onClick != null) {
@@ -120,7 +122,7 @@ fun UserAvatar(
url = "$REVOLT_BASE/users/$userId/default_avatar",
description = stringResource(id = R.string.avatar_alt, username),
modifier = Modifier
- .clip(CircleShape)
+ .clip(RoundedCornerShape(GlobalState.avatarRadius))
.size(size)
.then(
if (onLongClick != null || onClick != null) {
diff --git a/app/src/main/java/chat/revolt/components/screens/settings/appearance/CornerRadiusPicker.kt b/app/src/main/java/chat/revolt/components/screens/settings/appearance/CornerRadiusPicker.kt
new file mode 100644
index 00000000..be84e224
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/screens/settings/appearance/CornerRadiusPicker.kt
@@ -0,0 +1,191 @@
+package chat.revolt.components.screens.settings.appearance
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+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.draw.clip
+import androidx.compose.ui.graphics.Color
+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
+
+enum class CornerRadiusPreset(val percentage: Int) {
+ SHARP(0),
+ ROUNDED(15),
+ CIRCULAR(50),
+}
+
+@Composable
+fun CornerRadiusPicker(percentage: Int, onUpdate: (Int) -> Unit, modifier: Modifier = Modifier) {
+ var showOtherModal by remember { mutableStateOf(false) }
+
+ if (showOtherModal) {
+ var sliderPosition by remember { mutableStateOf(percentage.toFloat()) }
+ AlertDialog(
+ onDismissRequest = { showOtherModal = false },
+ title = {
+ Text(
+ text = stringResource(R.string.corner_radius_picker_choose_radius),
+ )
+ },
+ text = {
+ Column {
+ Spacer(modifier = Modifier.size(16.dp))
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(sliderPosition.toInt()))
+ .background(MaterialTheme.colorScheme.primary)
+ .size(64.dp),
+ )
+
+ Text(
+ sliderPosition.toInt().toString(),
+ style = MaterialTheme.typography.labelLarge.copy(
+ fontFeatureSettings = "tnum"
+ ),
+ )
+
+ Slider(
+ value = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = 0f..50f,
+ steps = 51
+ )
+ }
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ showOtherModal = false
+ onUpdate(sliderPosition.toInt())
+ }
+ ) {
+ Text(stringResource(R.string.corner_radius_picker_choose_radius_yes))
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = {
+ showOtherModal = false
+ }
+ ) {
+ Text(stringResource(R.string.corner_radius_picker_choose_radius_cancel))
+ }
+ }
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = modifier.clip(MaterialTheme.shapes.medium)
+ ) {
+ CornerRadiusPickerElement(
+ selected = percentage == CornerRadiusPreset.SHARP.percentage,
+ onSelect = {
+ onUpdate(CornerRadiusPreset.SHARP.percentage)
+ },
+ icon = painterResource(R.drawable.ux_corner_sharp),
+ label = stringResource(R.string.corner_radius_picker_sharp),
+ )
+ CornerRadiusPickerElement(
+ selected = percentage == CornerRadiusPreset.ROUNDED.percentage,
+ onSelect = {
+ onUpdate(CornerRadiusPreset.ROUNDED.percentage)
+ },
+ icon = painterResource(R.drawable.ux_corner_rounded),
+ label = stringResource(R.string.corner_radius_picker_rounded),
+ )
+ CornerRadiusPickerElement(
+ selected = percentage == CornerRadiusPreset.CIRCULAR.percentage,
+ onSelect = {
+ onUpdate(CornerRadiusPreset.CIRCULAR.percentage)
+ },
+ icon = painterResource(R.drawable.ux_corner_circular),
+ label = stringResource(R.string.corner_radius_picker_circular),
+ )
+ CornerRadiusPickerElement(
+ selected = percentage !in CornerRadiusPreset.entries.map { it.percentage },
+ onSelect = {
+ showOtherModal = true
+ },
+ icon = painterResource(R.drawable.ux_corner_other),
+ label = if (percentage !in CornerRadiusPreset.entries.map { it.percentage }) {
+ percentage.toString()
+ } else stringResource(R.string.corner_radius_picker_other),
+ highlightLabel = percentage !in CornerRadiusPreset.entries.map { it.percentage },
+ )
+ }
+}
+
+@Composable
+fun RowScope.CornerRadiusPickerElement(
+ selected: Boolean,
+ onSelect: () -> Unit,
+ icon: Painter,
+ label: String,
+ modifier: Modifier = Modifier,
+ highlightLabel: Boolean = false,
+) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier
+ .clip(MaterialTheme.shapes.medium)
+ .clickable { onSelect() }
+ .weight(1f)
+ .background(
+ if (selected)
+ MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
+ else
+ Color.Transparent
+ )
+ .padding(vertical = 8.dp)
+ ) {
+ Icon(
+ painter = icon,
+ contentDescription = null,
+ tint = if (selected)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
+ )
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelMedium,
+ color = if (highlightLabel)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.onSurface,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt
index aa7fa68c..2b1a7696 100644
--- a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt
@@ -75,6 +75,7 @@ import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.generic.ListHeader
import chat.revolt.components.screens.settings.appearance.ColourChip
+import chat.revolt.components.screens.settings.appearance.CornerRadiusPicker
import chat.revolt.ui.theme.ClearRippleTheme
import chat.revolt.ui.theme.OverridableColourScheme
import chat.revolt.ui.theme.Theme
@@ -111,6 +112,13 @@ class AppearanceSettingsScreenViewModel @Inject constructor(
}
}
+ fun saveNewAvatarRadius(radius: Int) {
+ GlobalState.avatarRadius = radius
+ viewModelScope.launch {
+ SyncedSettings.updateAndroid(SyncedSettings.android.copy(avatarRadius = radius))
+ }
+ }
+
fun updateColourOverrides(fieldName: String, value: Int?) {
viewModelScope.launch {
val overrides = SyncedSettings.android.copy().colourOverrides
@@ -389,6 +397,25 @@ fun AppearanceSettingsScreen(
}
}
+ ListHeader {
+ Text(stringResource(R.string.settings_appearance_avatar_shape))
+ }
+
+ Text(
+ stringResource(R.string.settings_appearance_avatar_shape_description),
+ modifier = Modifier
+ .padding(vertical = 8.dp, horizontal = 16.dp),
+ )
+
+ Box(Modifier.padding(horizontal = 16.dp)) {
+ CornerRadiusPicker(
+ percentage = GlobalState.avatarRadius,
+ onUpdate = {
+ viewModel.saveNewAvatarRadius(it)
+ }
+ )
+ }
+
Spacer(modifier = Modifier.height(20.dp))
Row(
diff --git a/app/src/main/res/drawable/ux_corner_circular.xml b/app/src/main/res/drawable/ux_corner_circular.xml
new file mode 100644
index 00000000..63e7e334
--- /dev/null
+++ b/app/src/main/res/drawable/ux_corner_circular.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ux_corner_other.xml b/app/src/main/res/drawable/ux_corner_other.xml
new file mode 100644
index 00000000..ecfd031f
--- /dev/null
+++ b/app/src/main/res/drawable/ux_corner_other.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ux_corner_rounded.xml b/app/src/main/res/drawable/ux_corner_rounded.xml
new file mode 100644
index 00000000..c6bfc303
--- /dev/null
+++ b/app/src/main/res/drawable/ux_corner_rounded.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ux_corner_sharp.xml b/app/src/main/res/drawable/ux_corner_sharp.xml
new file mode 100644
index 00000000..cc0c7892
--- /dev/null
+++ b/app/src/main/res/drawable/ux_corner_sharp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 55e6e34c..127beb37 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -466,6 +466,14 @@
Pick media…
Remove
+ Sharp
+ Rounded
+ Circular
+ Other
+ Choose radius
+ Use
+ Cancel
+
Close skin tone menu
No skin tone
Light skin tone
@@ -516,6 +524,9 @@
Material You (unsupported)
Material You is not supported on this device.
+ Profile Picture Shape
+ Choose the rounding grade for profile pictures, including in chat and profiles. This applies to all users.
+
Colour overrides
Primary
On Primary
@@ -597,5 +608,5 @@
This is not a valid share intent.
This attachment is too large for Revolt (max. $1$s).
Search channels
- Please select a channel to share to.
+ m select a channel to share to.