feat: add status functionality to status sheet

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-22 20:26:31 +02:00
parent 717e9f7890
commit f942218b6a
5 changed files with 178 additions and 42 deletions

View File

@ -5,10 +5,18 @@ import chat.revolt.api.RevoltError
import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson import chat.revolt.api.RevoltJson
import chat.revolt.api.schemas.Profile import chat.revolt.api.schemas.Profile
import chat.revolt.api.schemas.Status
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import io.ktor.client.request.get 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.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonElement
suspend fun fetchSelf(): User { suspend fun fetchSelf(): User {
val response = RevoltHttp.get("/users/@me") val response = RevoltHttp.get("/users/@me")
@ -33,6 +41,41 @@ suspend fun fetchSelf(): User {
return user return user
} }
suspend fun patchSelf(
status: Status? = null,
pure: Boolean = false,
) {
val body = mutableMapOf<String, JsonElement>()
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 { suspend fun fetchUser(id: String): User {
val res = RevoltHttp.get("/users/$id") val res = RevoltHttp.get("/users/$id")
@ -81,4 +124,4 @@ suspend fun fetchUserProfile(id: String): Profile {
} }
return RevoltJson.decodeFromString(Profile.serializer(), response) return RevoltJson.decodeFromString(Profile.serializer(), response)
} }

View File

@ -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 { fun presenceColour(presence: Presence): Color {
return when (presence) { return when (presence) {
Presence.Online -> Color(0xFF00C853) Presence.Online -> Color(0xFF3ABF7E)
Presence.Idle -> Color(0xFFFFD600) Presence.Idle -> Color(0xFFF39F00)
Presence.Dnd -> Color(0xFFD50000) Presence.Dnd -> Color(0xFFF84848)
Presence.Focus -> Color(0xFF0091EA) Presence.Focus -> Color(0xFF4799F0)
Presence.Offline -> Color(0xff546e7a) Presence.Offline -> Color(0xFFA5A5A5)
} }
} }

View File

@ -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))
)
}
}

View File

@ -1,11 +1,9 @@
package chat.revolt.sheets package chat.revolt.sheets
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI 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.SheetClickable
import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.asApiName
import chat.revolt.components.generic.presenceFromStatus 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 @Composable
fun StatusSheet(onBeforeNavigation: () -> Unit, onGoSettings: () -> Unit) { fun StatusSheet(onBeforeNavigation: () -> Unit, onGoSettings: () -> Unit) {
val selfUser = RevoltAPI.userCache[RevoltAPI.selfId]!! val selfUser = RevoltAPI.userCache[RevoltAPI.selfId]!!
val scope = rememberCoroutineScope()
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Row( UserOverview(selfUser)
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
)
)
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( StatusPicker(
text = AnnotatedString.Builder().apply { currentStatus = presenceFromStatus(selfUser.status?.presence, selfUser.online ?: false),
if (selfUser.displayName != null) { onStatusChange = {
pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) onBeforeNavigation()
append(selfUser.displayName) scope.launch {
pop() patchSelf(status = selfUser.status?.copy(presence = it.asApiName()))
append("\n") }
} }
append("${selfUser.username}") )
pushStyle(SpanStyle(fontWeight = FontWeight.ExtraLight))
append("#${selfUser.discriminator}")
pop()
}.toAnnotatedString()
)
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))

View File

@ -181,6 +181,7 @@
<string name="reconnecting">Reconnecting…</string> <string name="reconnecting">Reconnecting…</string>
<string name="reconnected">Reconnected</string> <string name="reconnected">Reconnected</string>
<string name="status">Status</string>
<string name="status_online">Online</string> <string name="status_online">Online</string>
<string name="status_idle">Idle</string> <string name="status_idle">Idle</string>
<string name="status_focus">Focus</string> <string name="status_focus">Focus</string>