feat: profile settings (set pfp and background)
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
fe024004cd
commit
771fc74cc1
|
|
@ -191,7 +191,6 @@ dependencies {
|
||||||
// Accompanist - Jetpack Compose Extensions
|
// Accompanist - Jetpack Compose Extensions
|
||||||
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
|
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
|
||||||
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
||||||
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
|
|
||||||
|
|
||||||
// KTOR - HTTP+WebSocket Library
|
// KTOR - HTTP+WebSocket Library
|
||||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import chat.revolt.screens.settings.AppearanceSettingsScreen
|
||||||
import chat.revolt.screens.settings.ChangelogsSettingsScreen
|
import chat.revolt.screens.settings.ChangelogsSettingsScreen
|
||||||
import chat.revolt.screens.settings.ClosedBetaUpdaterScreen
|
import chat.revolt.screens.settings.ClosedBetaUpdaterScreen
|
||||||
import chat.revolt.screens.settings.DebugSettingsScreen
|
import chat.revolt.screens.settings.DebugSettingsScreen
|
||||||
|
import chat.revolt.screens.settings.ProfileSettingsScreen
|
||||||
import chat.revolt.screens.settings.SessionSettingsScreen
|
import chat.revolt.screens.settings.SessionSettingsScreen
|
||||||
import chat.revolt.screens.settings.SettingsScreen
|
import chat.revolt.screens.settings.SettingsScreen
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
|
|
@ -143,6 +144,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
||||||
composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
|
composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
|
||||||
|
|
||||||
composable("settings") { SettingsScreen(navController) }
|
composable("settings") { SettingsScreen(navController) }
|
||||||
|
composable("settings/profile") { ProfileSettingsScreen(navController) }
|
||||||
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
||||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,13 @@ object RealtimeSocket {
|
||||||
val existing = RevoltAPI.userCache[userUpdateFrame.id]
|
val existing = RevoltAPI.userCache[userUpdateFrame.id]
|
||||||
?: return // if we don't have the user no point in updating it
|
?: return // if we don't have the user no point in updating it
|
||||||
|
|
||||||
|
if (userUpdateFrame.clear != null) {
|
||||||
|
if (userUpdateFrame.clear.contains("Avatar")) {
|
||||||
|
RevoltAPI.userCache[userUpdateFrame.id] =
|
||||||
|
existing.copy(avatar = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RevoltAPI.userCache[userUpdateFrame.id] =
|
RevoltAPI.userCache[userUpdateFrame.id] =
|
||||||
existing.mergeWithPartial(userUpdateFrame.data)
|
existing.mergeWithPartial(userUpdateFrame.data)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@ suspend fun uploadToAutumn(
|
||||||
val error = RevoltJson.decodeFromString(AutumnError.serializer(), response.bodyAsText())
|
val error = RevoltJson.decodeFromString(AutumnError.serializer(), response.bodyAsText())
|
||||||
throw Exception(error.type)
|
throw Exception(error.type)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (response.status.value == 429) {
|
||||||
|
throw Exception("Rate limited")
|
||||||
|
}
|
||||||
throw Exception("Unknown error")
|
throw Exception("Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
import kotlinx.serialization.builtins.MapSerializer
|
import kotlinx.serialization.builtins.MapSerializer
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
@ -41,12 +42,47 @@ suspend fun fetchSelf(): User {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun patchSelf(status: Status? = null, pure: Boolean = false) {
|
suspend fun patchSelf(
|
||||||
|
status: Status? = null,
|
||||||
|
avatar: String? = null,
|
||||||
|
background: String? = null,
|
||||||
|
bio: String? = null,
|
||||||
|
remove: List<String>? = null,
|
||||||
|
pure: Boolean = false
|
||||||
|
) {
|
||||||
val body = mutableMapOf<String, JsonElement>()
|
val body = mutableMapOf<String, JsonElement>()
|
||||||
|
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
body["status"] = RevoltJson.encodeToJsonElement(Status.serializer(), status)
|
body["status"] = RevoltJson.encodeToJsonElement(Status.serializer(), status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avatar != null) {
|
||||||
|
body["avatar"] = RevoltJson.encodeToJsonElement(String.serializer(), avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (background != null || bio != null) {
|
||||||
|
val profileMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
if (background != null) {
|
||||||
|
profileMap["background"] = background
|
||||||
|
}
|
||||||
|
if (bio != null) {
|
||||||
|
profileMap["bio"] = bio
|
||||||
|
}
|
||||||
|
|
||||||
|
body["profile"] = RevoltJson.encodeToJsonElement(
|
||||||
|
MapSerializer(
|
||||||
|
String.serializer(),
|
||||||
|
String.serializer()
|
||||||
|
),
|
||||||
|
profileMap
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove != null) {
|
||||||
|
body["remove"] = RevoltJson.encodeToJsonElement(ListSerializer(String.serializer()), remove)
|
||||||
|
}
|
||||||
|
|
||||||
val response = RevoltHttp.patch("/users/@me") {
|
val response = RevoltHttp.patch("/users/@me") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(
|
setBody(
|
||||||
|
|
@ -122,4 +158,4 @@ suspend fun fetchUserProfile(id: String): Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
return RevoltJson.decodeFromString(Profile.serializer(), response)
|
return RevoltJson.decodeFromString(Profile.serializer(), response)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
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.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.revolt.R
|
||||||
|
import com.bumptech.glide.integration.compose.CrossFade
|
||||||
|
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||||
|
import com.bumptech.glide.integration.compose.GlideImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InlineMediaPicker(
|
||||||
|
currentModel: Any?,
|
||||||
|
mimeType: String = "image/*",
|
||||||
|
circular: Boolean = false,
|
||||||
|
onPick: (Uri) -> Unit,
|
||||||
|
canRemove: Boolean = true,
|
||||||
|
onRemove: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
if (circular) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
InlineMediaPickerMediaPicker(
|
||||||
|
currentModel = currentModel,
|
||||||
|
mimeType = mimeType,
|
||||||
|
circular = true,
|
||||||
|
onPick = onPick
|
||||||
|
)
|
||||||
|
|
||||||
|
if (canRemove) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onRemove()
|
||||||
|
},
|
||||||
|
enabled = currentModel != null
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.inline_media_picker_remove)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column {
|
||||||
|
InlineMediaPickerMediaPicker(
|
||||||
|
currentModel = currentModel,
|
||||||
|
mimeType = mimeType,
|
||||||
|
circular = false,
|
||||||
|
onPick = onPick
|
||||||
|
)
|
||||||
|
|
||||||
|
if (canRemove) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onRemove()
|
||||||
|
},
|
||||||
|
enabled = currentModel != null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.inline_media_picker_remove)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.inline_media_picker_remove),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlideComposeApi::class)
|
||||||
|
@Composable
|
||||||
|
fun InlineMediaPickerMediaPicker(
|
||||||
|
currentModel: Any?,
|
||||||
|
mimeType: String = "image/*",
|
||||||
|
circular: Boolean = false,
|
||||||
|
onPick: (Uri) -> Unit
|
||||||
|
) {
|
||||||
|
val documentsUiLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
onPick(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentModel != null) {
|
||||||
|
GlideImage(
|
||||||
|
model = currentModel,
|
||||||
|
contentDescription = stringResource(R.string.inline_media_picker_current_description),
|
||||||
|
contentScale = if (circular) ContentScale.Crop else ContentScale.FillWidth,
|
||||||
|
modifier = if (circular) {
|
||||||
|
Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.width(82.dp)
|
||||||
|
.height(82.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.width(480.dp)
|
||||||
|
.height(140.dp)
|
||||||
|
}.clickable {
|
||||||
|
documentsUiLauncher.launch(mimeType)
|
||||||
|
},
|
||||||
|
transition = CrossFade,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = if (circular) {
|
||||||
|
Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.width(82.dp)
|
||||||
|
.height(82.dp)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.width(480.dp)
|
||||||
|
.height(140.dp)
|
||||||
|
}
|
||||||
|
.clickable {
|
||||||
|
documentsUiLauncher.launch(mimeType)
|
||||||
|
}
|
||||||
|
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.4f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (circular) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(R.string.inline_media_picker_no_media_placeholder)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.inline_media_picker_no_media_placeholder),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ import chat.revolt.api.internals.SpecialUsers
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
import chat.revolt.api.internals.solidColor
|
import chat.revolt.api.internals.solidColor
|
||||||
import chat.revolt.api.routes.user.fetchUserProfile
|
import chat.revolt.api.routes.user.fetchUserProfile
|
||||||
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
import chat.revolt.api.schemas.Profile
|
import chat.revolt.api.schemas.Profile
|
||||||
import chat.revolt.api.schemas.User
|
import chat.revolt.api.schemas.User
|
||||||
import chat.revolt.components.generic.RemoteImage
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
|
@ -70,7 +71,12 @@ fun UserOverview(user: User) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RawUserOverview(user: User, profile: Profile? = null) {
|
fun RawUserOverview(
|
||||||
|
user: User,
|
||||||
|
profile: Profile? = null,
|
||||||
|
pfpUrl: String? = null,
|
||||||
|
backgroundUrl: String? = null
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var teamMemberFlair by remember { mutableStateOf<Brush?>(null) }
|
var teamMemberFlair by remember { mutableStateOf<Brush?>(null) }
|
||||||
|
|
||||||
|
|
@ -104,9 +110,10 @@ fun RawUserOverview(user: User, profile: Profile? = null) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
profile?.background?.let { background ->
|
(backgroundUrl ?: profile?.background)?.let { background ->
|
||||||
RemoteImage(
|
RemoteImage(
|
||||||
url = "$REVOLT_FILES/backgrounds/${background.id}",
|
url = backgroundUrl
|
||||||
|
?: "$REVOLT_FILES/backgrounds/${if (background is AutumnResource) background.id else null}",
|
||||||
description = null,
|
description = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(128.dp)
|
.height(128.dp)
|
||||||
|
|
@ -137,6 +144,7 @@ fun RawUserOverview(user: User, profile: Profile? = null) {
|
||||||
) {
|
) {
|
||||||
UserAvatar(
|
UserAvatar(
|
||||||
username = user.displayName ?: stringResource(id = R.string.unknown),
|
username = user.displayName ?: stringResource(id = R.string.unknown),
|
||||||
|
rawUrl = pfpUrl,
|
||||||
userId = user.id ?: ULID.makeSpecial(0),
|
userId = user.id ?: ULID.makeSpecial(0),
|
||||||
avatar = user.avatar,
|
avatar = user.avatar,
|
||||||
size = 48.dp,
|
size = 48.dp,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ package chat.revolt.screens.settings
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
|
@ -29,7 +32,6 @@ import chat.revolt.components.generic.PageHeader
|
||||||
import chat.revolt.components.screens.settings.appearance.ThemeChip
|
import chat.revolt.components.screens.settings.appearance.ThemeChip
|
||||||
import chat.revolt.ui.theme.Theme
|
import chat.revolt.ui.theme.Theme
|
||||||
import chat.revolt.ui.theme.systemSupportsDynamicColors
|
import chat.revolt.ui.theme.systemSupportsDynamicColors
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AppearanceSettingsScreenViewModel : ViewModel() {
|
class AppearanceSettingsScreenViewModel : ViewModel() {
|
||||||
|
|
@ -42,6 +44,7 @@ class AppearanceSettingsScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppearanceSettingsScreen(
|
fun AppearanceSettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
|
|
@ -80,14 +83,16 @@ fun AppearanceSettingsScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
FlowRow(
|
FlowRow(
|
||||||
mainAxisSpacing = 10.dp,
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
crossAxisSpacing = 10.dp
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
ThemeChip(
|
ThemeChip(
|
||||||
color = Color(0xff1c243c),
|
color = Color(0xff1c243c),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_revolt),
|
text = stringResource(id = R.string.settings_appearance_theme_revolt),
|
||||||
selected = GlobalState.theme == Theme.Revolt,
|
selected = GlobalState.theme == Theme.Revolt,
|
||||||
modifier = Modifier.weight(1f).testTag("set_theme_revolt")
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("set_theme_revolt")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.Revolt)
|
setNewTheme(Theme.Revolt)
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +101,9 @@ fun AppearanceSettingsScreen(
|
||||||
color = Color(0xfff7f7f7),
|
color = Color(0xfff7f7f7),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_light),
|
text = stringResource(id = R.string.settings_appearance_theme_light),
|
||||||
selected = GlobalState.theme == Theme.Light,
|
selected = GlobalState.theme == Theme.Light,
|
||||||
modifier = Modifier.weight(1f).testTag("set_theme_light")
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("set_theme_light")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.Light)
|
setNewTheme(Theme.Light)
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +112,9 @@ fun AppearanceSettingsScreen(
|
||||||
color = Color(0xff000000),
|
color = Color(0xff000000),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_amoled),
|
text = stringResource(id = R.string.settings_appearance_theme_amoled),
|
||||||
selected = GlobalState.theme == Theme.Amoled,
|
selected = GlobalState.theme == Theme.Amoled,
|
||||||
modifier = Modifier.weight(1f).testTag("set_theme_amoled")
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("set_theme_amoled")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.Amoled)
|
setNewTheme(Theme.Amoled)
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +123,9 @@ fun AppearanceSettingsScreen(
|
||||||
color = if (isSystemInDarkTheme()) Color(0xff1c243c) else Color(0xfff7f7f7),
|
color = if (isSystemInDarkTheme()) Color(0xff1c243c) else Color(0xfff7f7f7),
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_none),
|
text = stringResource(id = R.string.settings_appearance_theme_none),
|
||||||
selected = GlobalState.theme == Theme.None,
|
selected = GlobalState.theme == Theme.None,
|
||||||
modifier = Modifier.weight(1f).testTag("set_theme_none")
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("set_theme_none")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.None)
|
setNewTheme(Theme.None)
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +135,9 @@ fun AppearanceSettingsScreen(
|
||||||
color = dynamicDarkColorScheme(LocalContext.current).primary,
|
color = dynamicDarkColorScheme(LocalContext.current).primary,
|
||||||
text = stringResource(id = R.string.settings_appearance_theme_m3dynamic),
|
text = stringResource(id = R.string.settings_appearance_theme_m3dynamic),
|
||||||
selected = GlobalState.theme == Theme.M3Dynamic,
|
selected = GlobalState.theme == Theme.M3Dynamic,
|
||||||
modifier = Modifier.weight(1f).testTag("set_theme_m3dynamic")
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("set_theme_m3dynamic")
|
||||||
) {
|
) {
|
||||||
setNewTheme(Theme.M3Dynamic)
|
setNewTheme(Theme.M3Dynamic)
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +148,9 @@ fun AppearanceSettingsScreen(
|
||||||
id = R.string.settings_appearance_theme_m3dynamic_unsupported
|
id = R.string.settings_appearance_theme_m3dynamic_unsupported
|
||||||
),
|
),
|
||||||
selected = false,
|
selected = false,
|
||||||
modifier = Modifier.weight(1f).testTag("set_theme_m3dynamic_unsupported")
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag("set_theme_m3dynamic_unsupported")
|
||||||
) {
|
) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,306 @@
|
||||||
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
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.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.uploadToAutumn
|
||||||
|
import chat.revolt.api.routes.user.fetchUserProfile
|
||||||
|
import chat.revolt.api.routes.user.patchSelf
|
||||||
|
import chat.revolt.api.schemas.Profile
|
||||||
|
import chat.revolt.components.generic.InlineMediaPicker
|
||||||
|
import chat.revolt.components.generic.PageHeader
|
||||||
|
import chat.revolt.components.screens.settings.RawUserOverview
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
@Suppress("StaticFieldLeak")
|
||||||
|
class ProfileSettingsScreenViewModel @Inject constructor(@ApplicationContext val context: Context) :
|
||||||
|
ViewModel() {
|
||||||
|
var pfpModel by mutableStateOf<Any?>(null)
|
||||||
|
var currentProfile by mutableStateOf<Profile?>(null)
|
||||||
|
var pendingProfile by mutableStateOf<Profile?>(null)
|
||||||
|
var backgroundModel by mutableStateOf<Any?>(null)
|
||||||
|
var uploadProgress by mutableFloatStateOf(0f)
|
||||||
|
var uploadError by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
RevoltAPI.selfId?.let { self ->
|
||||||
|
RevoltAPI.userCache[self]?.avatar?.id?.let {
|
||||||
|
pfpModel = "https://autumn.revolt.chat/avatars/${it}"
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
currentProfile = fetchUserProfile(self)
|
||||||
|
currentProfile!!.background?.id?.let {
|
||||||
|
backgroundModel = "https://autumn.revolt.chat/backgrounds/${it}"
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingProfile = currentProfile!!.copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveNewPfp() {
|
||||||
|
uploadError = null
|
||||||
|
|
||||||
|
val uri = when (pfpModel) {
|
||||||
|
is Uri -> pfpModel as Uri
|
||||||
|
is String -> Uri.parse(pfpModel as String)
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mFile = File(context.cacheDir, uri.lastPathSegment ?: "avatar")
|
||||||
|
|
||||||
|
mFile.outputStream().use { output ->
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mime = context.contentResolver.getType(uri)
|
||||||
|
|
||||||
|
if (mime?.endsWith("webp") == true) {
|
||||||
|
uploadError = "WebP is not supported"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val id = uploadToAutumn(
|
||||||
|
mFile,
|
||||||
|
uri.lastPathSegment ?: "avatar",
|
||||||
|
"avatars",
|
||||||
|
ContentType.parse(mime ?: "image/*"),
|
||||||
|
onProgress = { soFar, outOf ->
|
||||||
|
uploadProgress = soFar.toFloat() / outOf.toFloat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
patchSelf(avatar = id)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uploadError = e.message
|
||||||
|
uploadProgress = 0f
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
pfpModel = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar?.id?.let {
|
||||||
|
"https://autumn.revolt.chat/avatars/${it}"
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveNewBackground() {
|
||||||
|
uploadError = null
|
||||||
|
|
||||||
|
val uri = when (backgroundModel) {
|
||||||
|
is Uri -> backgroundModel as Uri
|
||||||
|
is String -> Uri.parse(backgroundModel as String)
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mFile = File(context.cacheDir, uri.lastPathSegment ?: "background")
|
||||||
|
|
||||||
|
mFile.outputStream().use { output ->
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mime = context.contentResolver.getType(uri)
|
||||||
|
|
||||||
|
if (mime?.endsWith("webp") == true) {
|
||||||
|
uploadError = "WebP is not supported"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val id = uploadToAutumn(
|
||||||
|
mFile,
|
||||||
|
uri.lastPathSegment ?: "background",
|
||||||
|
"backgrounds",
|
||||||
|
ContentType.parse(mime ?: "image/*"),
|
||||||
|
onProgress = { soFar, outOf ->
|
||||||
|
uploadProgress = soFar.toFloat() / outOf.toFloat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
patchSelf(background = id)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uploadError = e.message
|
||||||
|
uploadProgress = 0f
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundModel = RevoltAPI.selfId?.let {
|
||||||
|
val profile = fetchUserProfile(it)
|
||||||
|
currentProfile = profile
|
||||||
|
pendingProfile = profile
|
||||||
|
|
||||||
|
profile.background?.id?.let {
|
||||||
|
"https://autumn.revolt.chat/backgrounds/${it}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePfp() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
patchSelf(remove = listOf("Avatar"))
|
||||||
|
pfpModel = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeBackground() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
patchSelf(remove = listOf("ProfileBackground"))
|
||||||
|
pfpModel = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ProfileSettingsScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: ProfileSettingsScreenViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.safeDrawingPadding()
|
||||||
|
) {
|
||||||
|
PageHeader(
|
||||||
|
text = stringResource(id = R.string.settings_profile),
|
||||||
|
showBackButton = true,
|
||||||
|
onBackButtonClicked = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
RevoltAPI.userCache[RevoltAPI.selfId]?.let {
|
||||||
|
RawUserOverview(
|
||||||
|
it,
|
||||||
|
viewModel.pendingProfile,
|
||||||
|
viewModel.pfpModel?.toString(),
|
||||||
|
viewModel.backgroundModel?.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = viewModel.uploadProgress > 0f) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = viewModel.uploadProgress,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 0.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = viewModel.uploadError != null) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.uploadError ?: "",
|
||||||
|
style = MaterialTheme.typography.labelLarge.copy(
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 0.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 0.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_profile_profile_picture),
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
|
InlineMediaPicker(
|
||||||
|
currentModel = viewModel.pfpModel,
|
||||||
|
circular = true,
|
||||||
|
onPick = {
|
||||||
|
viewModel.pfpModel = it.toString()
|
||||||
|
viewModel.saveNewPfp()
|
||||||
|
},
|
||||||
|
canRemove = true,
|
||||||
|
onRemove = {
|
||||||
|
viewModel.removePfp()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_profile_custom_background),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
|
InlineMediaPicker(
|
||||||
|
currentModel = viewModel.backgroundModel,
|
||||||
|
onPick = {
|
||||||
|
viewModel.backgroundModel = it.toString()
|
||||||
|
viewModel.saveNewBackground()
|
||||||
|
},
|
||||||
|
canRemove = true,
|
||||||
|
onRemove = {
|
||||||
|
viewModel.removeBackground()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,8 +33,8 @@ import chat.revolt.components.generic.SheetClickable
|
||||||
import chat.revolt.components.screens.settings.SelfUserOverview
|
import chat.revolt.components.screens.settings.SelfUserOverview
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsScreenViewModel @Inject constructor(
|
class SettingsScreenViewModel @Inject constructor(
|
||||||
|
|
@ -85,6 +85,25 @@ fun SettingsScreen(
|
||||||
modifier = Modifier.padding(bottom = 10.dp, start = 10.dp, top = 20.dp)
|
modifier = Modifier.padding(bottom = 10.dp, start = 10.dp, top = 20.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SheetClickable(
|
||||||
|
icon = { modifier ->
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_card_account_details_24dp),
|
||||||
|
contentDescription = stringResource(id = R.string.settings_profile),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { textStyle ->
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_profile),
|
||||||
|
style = textStyle
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag("settings_view_profile")
|
||||||
|
) {
|
||||||
|
navController.navigate("settings/profile")
|
||||||
|
}
|
||||||
|
|
||||||
SheetClickable(
|
SheetClickable(
|
||||||
icon = { modifier ->
|
icon = { modifier ->
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M2,3H22C23.05,3 24,3.95 24,5V19C24,20.05 23.05,21 22,21H2C0.95,21 0,20.05 0,19V5C0,3.95 0.95,3 2,3M14,6V7H22V6H14M14,8V9H21.5L22,9V8H14M14,10V11H21V10H14M8,13.91C6,13.91 2,15 2,17V18H14V17C14,15 10,13.91 8,13.91M8,6A3,3 0 0,0 5,9A3,3 0 0,0 8,12A3,3 0 0,0 11,9A3,3 0 0,0 8,6Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -367,6 +367,10 @@
|
||||||
<string name="file_picker_chip_documents">Attach a file</string>
|
<string name="file_picker_chip_documents">Attach a file</string>
|
||||||
<string name="file_picker_chip_camera">Take a photo</string>
|
<string name="file_picker_chip_camera">Take a photo</string>
|
||||||
|
|
||||||
|
<string name="inline_media_picker_current_description">Currently selected media</string>
|
||||||
|
<string name="inline_media_picker_no_media_placeholder">Pick media…</string>
|
||||||
|
<string name="inline_media_picker_remove">Remove</string>
|
||||||
|
|
||||||
<string name="emoji_picker_close_skin_tone_menu">Close skin tone menu</string>
|
<string name="emoji_picker_close_skin_tone_menu">Close skin tone menu</string>
|
||||||
<string name="emoji_picker_skin_tone_none">No skin tone</string>
|
<string name="emoji_picker_skin_tone_none">No skin tone</string>
|
||||||
<string name="emoji_picker_skin_tone_fitzpatrick_1_2">Light skin tone</string>
|
<string name="emoji_picker_skin_tone_fitzpatrick_1_2">Light skin tone</string>
|
||||||
|
|
@ -391,6 +395,10 @@
|
||||||
<string name="settings_category_miscellaneous">Miscellaneous</string>
|
<string name="settings_category_miscellaneous">Miscellaneous</string>
|
||||||
<string name="settings_category_last" translatable="false">Revolt v%1$s</string>
|
<string name="settings_category_last" translatable="false">Revolt v%1$s</string>
|
||||||
|
|
||||||
|
<string name="settings_profile">Profile</string>
|
||||||
|
<string name="settings_profile_profile_picture">Profile picture</string>
|
||||||
|
<string name="settings_profile_custom_background">Custom background</string>
|
||||||
|
|
||||||
<string name="settings_sessions">Sessions</string>
|
<string name="settings_sessions">Sessions</string>
|
||||||
<string name="settings_sessions_this_device">This Device</string>
|
<string name="settings_sessions_this_device">This Device</string>
|
||||||
<string name="settings_sessions_this_device_unavailable">The session of this device is unavailable. Please **log in again** to view your current session.</string>
|
<string name="settings_sessions_this_device_unavailable">The session of this device is unavailable. Please **log in again** to view your current session.</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue