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
|
||||
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
|
||||
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
||||
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
|
||||
|
||||
// KTOR - HTTP+WebSocket Library
|
||||
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.ClosedBetaUpdaterScreen
|
||||
import chat.revolt.screens.settings.DebugSettingsScreen
|
||||
import chat.revolt.screens.settings.ProfileSettingsScreen
|
||||
import chat.revolt.screens.settings.SessionSettingsScreen
|
||||
import chat.revolt.screens.settings.SettingsScreen
|
||||
import chat.revolt.ui.theme.RevoltTheme
|
||||
|
|
@ -143,6 +144,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
|||
composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
|
||||
|
||||
composable("settings") { SettingsScreen(navController) }
|
||||
composable("settings/profile") { ProfileSettingsScreen(navController) }
|
||||
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||
|
|
|
|||
|
|
@ -259,6 +259,13 @@ object RealtimeSocket {
|
|||
val existing = RevoltAPI.userCache[userUpdateFrame.id]
|
||||
?: 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] =
|
||||
existing.mergeWithPartial(userUpdateFrame.data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ suspend fun uploadToAutumn(
|
|||
val error = RevoltJson.decodeFromString(AutumnError.serializer(), response.bodyAsText())
|
||||
throw Exception(error.type)
|
||||
} catch (e: Exception) {
|
||||
if (response.status.value == 429) {
|
||||
throw Exception("Rate limited")
|
||||
}
|
||||
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 kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
|
@ -41,12 +42,47 @@ suspend fun fetchSelf(): 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>()
|
||||
|
||||
if (status != null) {
|
||||
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") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
|
|
@ -122,4 +158,4 @@ suspend fun fetchUserProfile(id: String): Profile {
|
|||
}
|
||||
|
||||
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.solidColor
|
||||
import chat.revolt.api.routes.user.fetchUserProfile
|
||||
import chat.revolt.api.schemas.AutumnResource
|
||||
import chat.revolt.api.schemas.Profile
|
||||
import chat.revolt.api.schemas.User
|
||||
import chat.revolt.components.generic.RemoteImage
|
||||
|
|
@ -70,7 +71,12 @@ fun UserOverview(user: User) {
|
|||
}
|
||||
|
||||
@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
|
||||
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(
|
||||
url = "$REVOLT_FILES/backgrounds/${background.id}",
|
||||
url = backgroundUrl
|
||||
?: "$REVOLT_FILES/backgrounds/${if (background is AutumnResource) background.id else null}",
|
||||
description = null,
|
||||
modifier = Modifier
|
||||
.height(128.dp)
|
||||
|
|
@ -137,6 +144,7 @@ fun RawUserOverview(user: User, profile: Profile? = null) {
|
|||
) {
|
||||
UserAvatar(
|
||||
username = user.displayName ?: stringResource(id = R.string.unknown),
|
||||
rawUrl = pfpUrl,
|
||||
userId = user.id ?: ULID.makeSpecial(0),
|
||||
avatar = user.avatar,
|
||||
size = 48.dp,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package chat.revolt.screens.settings
|
|||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.ui.theme.Theme
|
||||
import chat.revolt.ui.theme.systemSupportsDynamicColors
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AppearanceSettingsScreenViewModel : ViewModel() {
|
||||
|
|
@ -42,6 +44,7 @@ class AppearanceSettingsScreenViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AppearanceSettingsScreen(
|
||||
navController: NavController,
|
||||
|
|
@ -80,14 +83,16 @@ fun AppearanceSettingsScreen(
|
|||
)
|
||||
|
||||
FlowRow(
|
||||
mainAxisSpacing = 10.dp,
|
||||
crossAxisSpacing = 10.dp
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
ThemeChip(
|
||||
color = Color(0xff1c243c),
|
||||
text = stringResource(id = R.string.settings_appearance_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)
|
||||
}
|
||||
|
|
@ -96,7 +101,9 @@ fun AppearanceSettingsScreen(
|
|||
color = Color(0xfff7f7f7),
|
||||
text = stringResource(id = R.string.settings_appearance_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)
|
||||
}
|
||||
|
|
@ -105,7 +112,9 @@ fun AppearanceSettingsScreen(
|
|||
color = Color(0xff000000),
|
||||
text = stringResource(id = R.string.settings_appearance_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)
|
||||
}
|
||||
|
|
@ -114,7 +123,9 @@ fun AppearanceSettingsScreen(
|
|||
color = if (isSystemInDarkTheme()) Color(0xff1c243c) else Color(0xfff7f7f7),
|
||||
text = stringResource(id = R.string.settings_appearance_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)
|
||||
}
|
||||
|
|
@ -124,7 +135,9 @@ fun AppearanceSettingsScreen(
|
|||
color = dynamicDarkColorScheme(LocalContext.current).primary,
|
||||
text = stringResource(id = R.string.settings_appearance_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)
|
||||
}
|
||||
|
|
@ -135,7 +148,9 @@ fun AppearanceSettingsScreen(
|
|||
id = R.string.settings_appearance_theme_m3dynamic_unsupported
|
||||
),
|
||||
selected = false,
|
||||
modifier = Modifier.weight(1f).testTag("set_theme_m3dynamic_unsupported")
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.testTag("set_theme_m3dynamic_unsupported")
|
||||
) {
|
||||
Toast.makeText(
|
||||
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.persistence.KVStorage
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsScreenViewModel @Inject constructor(
|
||||
|
|
@ -85,6 +85,25 @@ fun SettingsScreen(
|
|||
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(
|
||||
icon = { modifier ->
|
||||
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_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_skin_tone_none">No 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_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_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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue