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 8773c899..b828bfb4 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 @@ -5,10 +5,18 @@ import chat.revolt.api.RevoltError import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltJson import chat.revolt.api.schemas.Profile +import chat.revolt.api.schemas.Status import chat.revolt.api.schemas.User import io.ktor.client.request.get +import io.ktor.client.request.patch +import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.contentType import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.JsonElement suspend fun fetchSelf(): User { val response = RevoltHttp.get("/users/@me") @@ -33,6 +41,41 @@ suspend fun fetchSelf(): User { return user } +suspend fun patchSelf( + status: Status? = null, + pure: Boolean = false, +) { + val body = mutableMapOf() + if (status != null) { + body["status"] = RevoltJson.encodeToJsonElement(Status.serializer(), status) + } + + val response = RevoltHttp.patch("/users/@me") { + contentType(ContentType.Application.Json) + setBody( + RevoltJson.encodeToString( + MapSerializer( + String.serializer(), + JsonElement.serializer() + ), body + ) + ) + } + .bodyAsText() + + if (RevoltAPI.selfId == null) { + throw Error("Self ID is null") + } + + val currentUser = RevoltAPI.userCache[RevoltAPI.selfId] ?: fetchSelf() + val newUserKeys = RevoltJson.decodeFromString(User.serializer(), response) + val mergedUser = currentUser.mergeWithPartial(newUserKeys) + + if (!pure) { + RevoltAPI.userCache[RevoltAPI.selfId!!] = mergedUser + } +} + suspend fun fetchUser(id: String): User { val res = RevoltHttp.get("/users/$id") @@ -81,4 +124,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/UserAvatar.kt b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt index b64b4826..e9c1181c 100644 --- a/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt +++ b/app/src/main/java/chat/revolt/components/generic/UserAvatar.kt @@ -45,13 +45,23 @@ fun presenceFromStatus(status: String?, online: Boolean = true): Presence { } } +fun Presence.asApiName(): String { + return when (this) { + Presence.Online -> "Online" + Presence.Idle -> "Idle" + Presence.Dnd -> "Busy" + Presence.Focus -> "Focus" + Presence.Offline -> "Invisible" + } +} + fun presenceColour(presence: Presence): Color { return when (presence) { - Presence.Online -> Color(0xFF00C853) - Presence.Idle -> Color(0xFFFFD600) - Presence.Dnd -> Color(0xFFD50000) - Presence.Focus -> Color(0xFF0091EA) - Presence.Offline -> Color(0xff546e7a) + Presence.Online -> Color(0xFF3ABF7E) + Presence.Idle -> Color(0xFFF39F00) + Presence.Dnd -> Color(0xFFF84848) + Presence.Focus -> Color(0xFF4799F0) + Presence.Offline -> Color(0xFFA5A5A5) } } diff --git a/app/src/main/java/chat/revolt/components/settings/profile/StatusPicker.kt b/app/src/main/java/chat/revolt/components/settings/profile/StatusPicker.kt new file mode 100644 index 00000000..c253a6ab --- /dev/null +++ b/app/src/main/java/chat/revolt/components/settings/profile/StatusPicker.kt @@ -0,0 +1,99 @@ +package chat.revolt.components.settings.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +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.res.stringResource +import androidx.compose.ui.unit.dp +import chat.revolt.R +import chat.revolt.components.generic.Presence +import chat.revolt.components.generic.presenceColour + +fun Presence.stringResource(): Int { + return when (this) { + Presence.Online -> R.string.status_online + Presence.Idle -> R.string.status_idle + Presence.Dnd -> R.string.status_dnd + Presence.Focus -> R.string.status_focus + Presence.Offline -> R.string.status_invisible + } +} + +@Composable +fun StatusPicker( + currentStatus: Presence, + onStatusChange: (Presence) -> Unit, + modifier: Modifier = Modifier +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + Text( + text = stringResource(R.string.status), + style = MaterialTheme.typography.labelLarge + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) + ) { + Presence.entries.forEach { + StatusButton( + presence = it, + selected = it == currentStatus, + onClick = onStatusChange, + modifier = modifier + ) + } + } + + Text( + text = stringResource(currentStatus.stringResource()), + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@Composable +fun StatusButton( + presence: Presence, + selected: Boolean, + onClick: (Presence) -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(48.dp) + .clickable { onClick(presence) } + .then( + if (selected) Modifier.background( + MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp) + ) else Modifier + ), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .size(24.dp) + .background(presenceColour(presence)) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/sheets/StatusSheet.kt b/app/src/main/java/chat/revolt/sheets/StatusSheet.kt index b2b9c1a8..86859917 100644 --- a/app/src/main/java/chat/revolt/sheets/StatusSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/StatusSheet.kt @@ -1,11 +1,9 @@ package chat.revolt.sheets import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -13,60 +11,45 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.revolt.R import chat.revolt.api.RevoltAPI -import chat.revolt.api.internals.ULID +import chat.revolt.api.routes.user.patchSelf import chat.revolt.components.generic.SheetClickable -import chat.revolt.components.generic.UserAvatar +import chat.revolt.components.generic.asApiName import chat.revolt.components.generic.presenceFromStatus +import chat.revolt.components.screens.settings.UserOverview +import chat.revolt.components.settings.profile.StatusPicker +import kotlinx.coroutines.launch @Composable fun StatusSheet(onBeforeNavigation: () -> Unit, onGoSettings: () -> Unit) { val selfUser = RevoltAPI.userCache[RevoltAPI.selfId]!! + val scope = rememberCoroutineScope() Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp) .verticalScroll(rememberScrollState()) ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - UserAvatar( - username = selfUser.displayName ?: stringResource(id = R.string.unknown), - userId = selfUser.id ?: ULID.makeSpecial(0), - avatar = selfUser.avatar, - size = 48.dp, - presence = presenceFromStatus( - selfUser.status?.presence, - selfUser.online ?: false - ) - ) + UserOverview(selfUser) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Text( - text = AnnotatedString.Builder().apply { - if (selfUser.displayName != null) { - pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) - append(selfUser.displayName) - pop() - append("\n") - } - append("${selfUser.username}") - pushStyle(SpanStyle(fontWeight = FontWeight.ExtraLight)) - append("#${selfUser.discriminator}") - pop() - }.toAnnotatedString() - ) - } + StatusPicker( + currentStatus = presenceFromStatus(selfUser.status?.presence, selfUser.online ?: false), + onStatusChange = { + onBeforeNavigation() + scope.launch { + patchSelf(status = selfUser.status?.copy(presence = it.asApiName())) + } + } + ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd01563e..354ad160 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -181,6 +181,7 @@ Reconnecting… Reconnected + Status Online Idle Focus