From 771fc74cc10995df43426e0bd5ad0f92bfae8bc9 Mon Sep 17 00:00:00 2001 From: Infi Date: Tue, 31 Oct 2023 20:07:12 +0100 Subject: [PATCH] feat: profile settings (set pfp and background) Signed-off-by: Infi --- app/build.gradle | 1 - .../chat/revolt/activities/MainActivity.kt | 2 + .../revolt/api/realtime/RealtimeSocket.kt | 7 + .../api/routes/microservices/autumn/Autumn.kt | 3 + .../java/chat/revolt/api/routes/user/User.kt | 40 ++- .../components/generic/InlineMediaPicker.kt | 179 ++++++++++ .../screens/settings/UserOverview.kt | 14 +- .../settings/AppearanceSettingsScreen.kt | 33 +- .../screens/settings/ProfileSettngsScreen.kt | 306 ++++++++++++++++++ .../revolt/screens/settings/SettingsScreen.kt | 21 +- .../drawable/ic_card_account_details_24dp.xml | 9 + app/src/main/res/values/strings.xml | 8 + 12 files changed, 607 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/chat/revolt/components/generic/InlineMediaPicker.kt create mode 100644 app/src/main/java/chat/revolt/screens/settings/ProfileSettngsScreen.kt create mode 100644 app/src/main/res/drawable/ic_card_account_details_24dp.xml diff --git a/app/build.gradle b/app/build.gradle index 25639041..43e0a457 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index 134cb536..44c3ec3a 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -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) } diff --git a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt index 264a3d16..495b53e5 100644 --- a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt +++ b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt @@ -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) } diff --git a/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt b/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt index 5644a80f..46079c37 100644 --- a/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt +++ b/app/src/main/java/chat/revolt/api/routes/microservices/autumn/Autumn.kt @@ -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") } } diff --git a/app/src/main/java/chat/revolt/api/routes/user/User.kt b/app/src/main/java/chat/revolt/api/routes/user/User.kt index 4960c80a..70bcf1e1 100644 --- a/app/src/main/java/chat/revolt/api/routes/user/User.kt +++ b/app/src/main/java/chat/revolt/api/routes/user/User.kt @@ -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? = null, + pure: Boolean = false +) { val body = mutableMapOf() + 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() + + 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) -} +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/generic/InlineMediaPicker.kt b/app/src/main/java/chat/revolt/components/generic/InlineMediaPicker.kt new file mode 100644 index 00000000..dbbb62d3 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/generic/InlineMediaPicker.kt @@ -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 + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/screens/settings/UserOverview.kt b/app/src/main/java/chat/revolt/components/screens/settings/UserOverview.kt index abb1eff0..d7605051 100644 --- a/app/src/main/java/chat/revolt/components/screens/settings/UserOverview.kt +++ b/app/src/main/java/chat/revolt/components/screens/settings/UserOverview.kt @@ -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(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, 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 a7fce6e3..00df9ceb 100644 --- a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt @@ -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, diff --git a/app/src/main/java/chat/revolt/screens/settings/ProfileSettngsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/ProfileSettngsScreen.kt new file mode 100644 index 00000000..14179844 --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/settings/ProfileSettngsScreen.kt @@ -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(null) + var currentProfile by mutableStateOf(null) + var pendingProfile by mutableStateOf(null) + var backgroundModel by mutableStateOf(null) + var uploadProgress by mutableFloatStateOf(0f) + var uploadError by mutableStateOf(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() + } + ) + } + } + } + } +} diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt index 963ddf32..4c2acfa2 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -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( diff --git a/app/src/main/res/drawable/ic_card_account_details_24dp.xml b/app/src/main/res/drawable/ic_card_account_details_24dp.xml new file mode 100644 index 00000000..12a38cd8 --- /dev/null +++ b/app/src/main/res/drawable/ic_card_account_details_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f99d0374..6ebed104 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -367,6 +367,10 @@ Attach a file Take a photo + Currently selected media + Pick media… + Remove + Close skin tone menu No skin tone Light skin tone @@ -391,6 +395,10 @@ Miscellaneous Revolt v%1$s + Profile + Profile picture + Custom background + Sessions This Device The session of this device is unavailable. Please **log in again** to view your current session.