feat: pronouns
This commit is contained in:
parent
3ac331ac1f
commit
46987baec9
|
|
@ -478,15 +478,17 @@ object RealtimeSocket {
|
|||
val existing = StoatAPI.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")) {
|
||||
StoatAPI.userCache[userUpdateFrame.id] =
|
||||
existing.copy(avatar = null)
|
||||
var updated = existing.mergeWithPartial(userUpdateFrame.data)
|
||||
|
||||
userUpdateFrame.clear?.forEach {
|
||||
updated = when (it) {
|
||||
"Avatar" -> updated.copy(avatar = null)
|
||||
"Pronouns" -> updated.copy(pronouns = null)
|
||||
else -> updated
|
||||
}
|
||||
}
|
||||
|
||||
StoatAPI.userCache[userUpdateFrame.id] =
|
||||
existing.mergeWithPartial(userUpdateFrame.data)
|
||||
StoatAPI.userCache[userUpdateFrame.id] = updated
|
||||
}
|
||||
|
||||
"UserRelationship" -> {
|
||||
|
|
@ -756,6 +758,7 @@ object RealtimeSocket {
|
|||
when (it) {
|
||||
"Avatar" -> updated = updated.copy(avatar = null)
|
||||
"Nickname" -> updated = updated.copy(nickname = null)
|
||||
"Pronouns" -> updated = updated.copy(pronouns = null)
|
||||
else -> Log.e("RealtimeSocket", "Unknown server member clear field: $it")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ data class UserUpdateFrame(
|
|||
val type: String = "UserUpdate",
|
||||
val id: String,
|
||||
val data: User,
|
||||
val clear: List<String>? = null // "ProfileContent", "ProfileBackground", "StatusText" or "Avatar"
|
||||
val clear: List<String>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ suspend fun fetchSelf(): User {
|
|||
|
||||
suspend fun patchSelf(
|
||||
status: Status? = null,
|
||||
pronouns: String? = null,
|
||||
avatar: String? = null,
|
||||
background: String? = null,
|
||||
bio: String? = null,
|
||||
|
|
@ -57,6 +58,10 @@ suspend fun patchSelf(
|
|||
body["status"] = StoatJson.encodeToJsonElement(Status.serializer(), status)
|
||||
}
|
||||
|
||||
if (pronouns != null) {
|
||||
body["pronouns"] = StoatJson.encodeToJsonElement(String.serializer(), pronouns)
|
||||
}
|
||||
|
||||
if (avatar != null) {
|
||||
body["avatar"] = StoatJson.encodeToJsonElement(String.serializer(), avatar)
|
||||
}
|
||||
|
|
@ -104,7 +109,11 @@ suspend fun patchSelf(
|
|||
|
||||
val currentUser = StoatAPI.userCache[StoatAPI.selfId] ?: fetchSelf()
|
||||
val newUserKeys = StoatJson.decodeFromString(User.serializer(), response)
|
||||
val mergedUser = currentUser.mergeWithPartial(newUserKeys)
|
||||
var mergedUser = currentUser.mergeWithPartial(newUserKeys)
|
||||
|
||||
if ("Pronouns" in remove.orEmpty()) {
|
||||
mergedUser = mergedUser.copy(pronouns = null)
|
||||
}
|
||||
|
||||
if (!pure) {
|
||||
StoatAPI.userCache[StoatAPI.selfId!!] = mergedUser
|
||||
|
|
@ -159,4 +168,4 @@ suspend fun fetchUserProfile(id: String): Profile {
|
|||
}
|
||||
|
||||
return StoatJson.decodeFromString(Profile.serializer(), response)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,22 @@ fun authorName(message: MessageSchema): String {
|
|||
?: stringResource(R.string.unknown)
|
||||
}
|
||||
|
||||
fun authorPronouns(message: MessageSchema, author: User): String? {
|
||||
val serverId = StoatAPI.channelCache[message.channel]?.server
|
||||
?: return author.pronouns
|
||||
|
||||
val memberPronouns = message.author
|
||||
?.let { StoatAPI.members.getMember(serverId, it) }
|
||||
?.pronouns
|
||||
|
||||
return memberPronouns ?: author.pronouns
|
||||
}
|
||||
|
||||
internal fun messageTimestampText(pronouns: String?, timestamp: String): String {
|
||||
val visiblePronouns = pronouns?.trim()?.takeIf { it.isNotEmpty() }
|
||||
return visiblePronouns?.let { "$it · $timestamp" } ?: timestamp
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun authorAvatarUrl(message: MessageSchema): String? {
|
||||
if (message.masquerade?.avatar != null) {
|
||||
|
|
@ -449,7 +465,10 @@ fun Message(
|
|||
Spacer(modifier = Modifier.width(5.dp))
|
||||
|
||||
Text(
|
||||
text = formatLongAsTime(ULID.asTimestamp(message.id!!)),
|
||||
text = messageTimestampText(
|
||||
pronouns = authorPronouns(message, author),
|
||||
timestamp = formatLongAsTime(ULID.asTimestamp(message.id!!))
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
|
||||
maxLines = 1,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -29,6 +30,7 @@ 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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.stoat.R
|
||||
import chat.stoat.api.StoatAPI
|
||||
|
|
@ -113,6 +115,9 @@ fun RawUserOverview(
|
|||
)
|
||||
) {
|
||||
val background = backgroundUrl ?: profile?.background
|
||||
val contentColour = if (background != null) Color.White else LocalContentColor.current
|
||||
val pronouns = user.pronouns?.trim()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
if (background != null) {
|
||||
RemoteImage(
|
||||
url = backgroundUrl
|
||||
|
|
@ -179,8 +184,26 @@ fun RawUserOverview(
|
|||
append("#${user.discriminator}")
|
||||
pop()
|
||||
}.toAnnotatedString(),
|
||||
color = if (profile?.background != null) Color.White else LocalContentColor.current
|
||||
color = contentColour,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
pronouns?.let {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = it,
|
||||
color = contentColour,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Bottom)
|
||||
.widthIn(max = 140.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,18 @@ class ProfileSettingsScreenViewModel(val context: Application) :
|
|||
var uploadProgress by mutableFloatStateOf(0f)
|
||||
var uploadError by mutableStateOf<String?>(null)
|
||||
var bioError by mutableStateOf<String?>(null)
|
||||
var currentPronouns by mutableStateOf<String?>(null)
|
||||
var pendingPronouns by mutableStateOf("")
|
||||
var pronounsError by mutableStateOf<String?>(null)
|
||||
|
||||
init {
|
||||
StoatAPI.selfId?.let { self ->
|
||||
StoatAPI.userCache[self]?.avatar?.id?.let {
|
||||
pfpModel = "$STOAT_FILES/avatars/${it}"
|
||||
StoatAPI.userCache[self]?.let { user ->
|
||||
user.avatar?.id?.let {
|
||||
pfpModel = "$STOAT_FILES/avatars/${it}"
|
||||
}
|
||||
currentPronouns = user.pronouns
|
||||
pendingPronouns = user.pronouns.orEmpty()
|
||||
}
|
||||
viewModelScope.launch {
|
||||
currentProfile = fetchUserProfile(self)
|
||||
|
|
@ -226,6 +233,26 @@ class ProfileSettingsScreenViewModel(val context: Application) :
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun savePronouns() {
|
||||
pronounsError = null
|
||||
val normalizedPronouns = pendingPronouns.trim()
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
if (normalizedPronouns.isEmpty()) {
|
||||
patchSelf(remove = listOf("Pronouns"))
|
||||
} else {
|
||||
patchSelf(pronouns = normalizedPronouns)
|
||||
}
|
||||
|
||||
currentPronouns = normalizedPronouns.ifEmpty { null }
|
||||
pendingPronouns = normalizedPronouns
|
||||
} catch (e: Exception) {
|
||||
pronounsError = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
|
|
@ -380,6 +407,62 @@ fun ProfileSettingsScreen(
|
|||
modifier = Modifier
|
||||
.padding(start = 20.dp, end = 20.dp, top = 0.dp, bottom = 20.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.pendingPronouns,
|
||||
onValueChange = { value ->
|
||||
if (value.length <= 24) {
|
||||
viewModel.pendingPronouns = value
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_profile_pronouns),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
},
|
||||
isError = viewModel.pronounsError != null,
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = viewModel.pronounsError != null) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = viewModel.pronounsError ?: "",
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.error
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 0.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.savePronouns()
|
||||
},
|
||||
enabled = viewModel.pendingPronouns.trim().ifEmpty { null } !=
|
||||
viewModel.currentPronouns,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check_24dp),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_profile_save),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.pendingProfile?.content ?: "",
|
||||
onValueChange = { value ->
|
||||
|
|
|
|||
|
|
@ -810,6 +810,7 @@
|
|||
<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_profile_pronouns">Pronouns</string>
|
||||
<string name="settings_profile_save">Save</string>
|
||||
|
||||
<string name="settings_sessions">Sessions</string>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ data class Member(
|
|||
val avatar: AutumnResource? = null,
|
||||
val roles: List<String>? = null,
|
||||
val nickname: String? = null,
|
||||
val pronouns: String? = null,
|
||||
|
||||
val timeout: String? = null
|
||||
) {
|
||||
|
|
@ -45,6 +46,7 @@ data class Member(
|
|||
avatar = other.avatar ?: avatar,
|
||||
roles = other.roles ?: roles,
|
||||
nickname = other.nickname ?: nickname,
|
||||
pronouns = other.pronouns ?: pronouns,
|
||||
timeout = other.timeout ?: timeout
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ data class User(
|
|||
val discriminator: String? = null,
|
||||
@SerialName("display_name")
|
||||
val displayName: String? = null,
|
||||
val pronouns: String? = null,
|
||||
val avatar: AutumnResource? = null,
|
||||
val relations: List<Relation>? = null,
|
||||
val badges: Long? = null,
|
||||
|
|
@ -31,6 +32,7 @@ data class User(
|
|||
username = partial.username ?: username,
|
||||
discriminator = partial.discriminator ?: discriminator,
|
||||
displayName = partial.displayName ?: displayName,
|
||||
pronouns = partial.pronouns ?: pronouns,
|
||||
avatar = partial.avatar ?: avatar,
|
||||
relations = partial.relations ?: relations,
|
||||
badges = partial.badges ?: badges,
|
||||
|
|
@ -50,6 +52,7 @@ data class User(
|
|||
username = "Unknown User",
|
||||
discriminator = "0000",
|
||||
displayName = null,
|
||||
pronouns = null,
|
||||
avatar = null,
|
||||
badges = 0,
|
||||
status = null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue